(feat): redesign WASM module, add TypeScript SDK, migrate to pnpm
Some checks failed
CI / rustfmt (push) Successful in 17s
CI / wasm build (push) Successful in 1m16s
CI / clippy (push) Successful in 1m28s
CI / test (push) Successful in 1m48s
CI / example (push) Successful in 1m31s
CI / duplicate code (push) Failing after 33s
CI / web client (push) Failing after 34s
CI / cargo-machete (push) Successful in 1m18s
CI / cargo-deny (push) Failing after 3m2s
Some checks failed
CI / rustfmt (push) Successful in 17s
CI / wasm build (push) Successful in 1m16s
CI / clippy (push) Successful in 1m28s
CI / test (push) Successful in 1m48s
CI / example (push) Successful in 1m31s
CI / duplicate code (push) Failing after 33s
CI / web client (push) Failing after 34s
CI / cargo-machete (push) Successful in 1m18s
CI / cargo-deny (push) Failing after 3m2s
This commit is contained in:
parent
89a20044a5
commit
5caa1c9d5f
49 changed files with 3717 additions and 1501 deletions
|
|
@ -44,17 +44,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.cargo/bin:$PATH"
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml"
|
export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml"
|
||||||
cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings \
|
cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings -W unreachable-pub
|
||||||
-W unreachable-pub \
|
|
||||||
-W clippy::cognitive_complexity \
|
|
||||||
-W clippy::missing_docs_in_private_items \
|
|
||||||
-W clippy::missing_errors_doc \
|
|
||||||
-W clippy::missing_panics_doc \
|
|
||||||
-W clippy::missing_safety_doc \
|
|
||||||
-W clippy::undocumented_unsafe_blocks \
|
|
||||||
-W clippy::pedantic \
|
|
||||||
-W clippy::restriction \
|
|
||||||
-A clippy::blanket_clippy_restriction_lints
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: test
|
name: test
|
||||||
|
|
@ -169,21 +159,21 @@ jobs:
|
||||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip
|
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip nodejs
|
||||||
|
|
||||||
- name: Install Bun
|
- name: Install pnpm
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://get.pnpm.io/install.sh | SHELL=/bin/sh sh -
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.bun/bin:$PATH"
|
export PATH="$HOME/.local/share/pnpm:$PATH"
|
||||||
bun install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run duplicate detector
|
- name: Run duplicate detector
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.bun/bin:$PATH"
|
export PATH="$HOME/.local/share/pnpm:$PATH"
|
||||||
bun run dup
|
pnpm run dup
|
||||||
|
|
||||||
web-client:
|
web-client:
|
||||||
name: web client
|
name: web client
|
||||||
|
|
@ -192,7 +182,7 @@ jobs:
|
||||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip build-essential pkg-config libssl-dev lld
|
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip nodejs build-essential pkg-config libssl-dev lld
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -209,18 +199,24 @@ jobs:
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: --cfg web_sys_unstable_apis
|
RUSTFLAGS: --cfg web_sys_unstable_apis
|
||||||
|
|
||||||
- name: Install Bun
|
- name: Install pnpm
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://get.pnpm.io/install.sh | SHELL=/bin/sh sh -
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: example/web-client
|
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.bun/bin:$PATH"
|
export PATH="$HOME/.local/share/pnpm:$PATH"
|
||||||
bun install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build TypeScript package
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$HOME/.local/share/pnpm:$PATH"
|
||||||
|
export MTP_TYPE_MAPS="$PWD/example/type-maps.yaml"
|
||||||
|
pnpm run build
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: --cfg web_sys_unstable_apis
|
||||||
|
|
||||||
- name: Build web client
|
- name: Build web client
|
||||||
working-directory: example/web-client
|
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.bun/bin:$PATH"
|
export PATH="$HOME/.local/share/pnpm:$PATH"
|
||||||
bun run build
|
pnpm --filter mtp-web-client run build
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,3 +2,5 @@
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
node_modules/
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tgz
|
||||||
|
|
|
||||||
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -535,6 +535,15 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -1035,7 +1044,6 @@ dependencies = [
|
||||||
name = "mtp-host"
|
name = "mtp-host"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
"mtp-common",
|
"mtp-common",
|
||||||
"mtp-crypto",
|
"mtp-crypto",
|
||||||
|
|
@ -1070,6 +1078,7 @@ name = "mtp-wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
"futures-channel",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"hex",
|
"hex",
|
||||||
|
|
@ -1081,7 +1090,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2159,16 +2167,6 @@ version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920"
|
checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.103"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-time"
|
name = "web-time"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -2,7 +2,43 @@
|
||||||
|
|
||||||
MTP is a modular transport protocol built on QUIC. It provides version-negotiable type maps, a binary codec, cryptographic primitives (classical and post-quantum), and host/client connection management with mutual authentication.
|
MTP is a modular transport protocol built on QUIC. It provides version-negotiable type maps, a binary codec, cryptographic primitives (classical and post-quantum), and host/client connection management with mutual authentication.
|
||||||
|
|
||||||
See the area-specific docs for [Native Client](./NATIVE-CLIENT.md), [WASM Client](./WASM-CLIENT.md), and [Host](./NATIVE-HOST.md)
|
See the area-specific docs for [Native Client](./docs/NATIVE-CLIENT.md), [WASM Client](./docs/WASM-CLIENT.md), [Host](./docs/NATIVE-HOST.md), and [Type Maps](./docs/TYPE-MAP.md).
|
||||||
|
|
||||||
|
## Browser SDK
|
||||||
|
|
||||||
|
The JavaScript package is `mtp`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MTPClient } from "mtp";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `mtp` for the SDK-first API, `mtp/raw` for generated WASM bindings, and `mtp/vite` for the Vite integration.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [mtp({ typeMaps: "./type-maps.yaml" })],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = await MTPClient.create({
|
||||||
|
url: "https://localhost:4433",
|
||||||
|
hostPublicKey,
|
||||||
|
credentials,
|
||||||
|
storage,
|
||||||
|
pings: true,
|
||||||
|
logger: (event) => console.log(event),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.subscribe("SomeType", (message) => console.log(message));
|
||||||
|
await client.connectOrRegister();
|
||||||
|
await client.send("SomeType", { value: "hello" });
|
||||||
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|
|
||||||
27
bun.lock
27
bun.lock
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "mtp-wasm",
|
|
||||||
"devDependencies": {
|
|
||||||
"jscpd": "5.0.11",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"cpd-darwin-arm64": ["cpd-darwin-arm64@5.0.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3QvH+4Dv7A7esVFM2tsRVWN3kn9EDu8dMYog6gYAVsCtxEf4xyxAwS/ef6LjC7/dh4+ATADFbg3H09A2fD//Qw=="],
|
|
||||||
|
|
||||||
"cpd-darwin-x64": ["cpd-darwin-x64@5.0.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-OvgM2ps0OFR5jUzx7+FK9URdJGxUzzM5KKk2F1V3vf1LooGDKwkivfIDyKsqEwp37zcbyUo7COvBpJXOT0dZmQ=="],
|
|
||||||
|
|
||||||
"cpd-linux-arm64-gnu": ["cpd-linux-arm64-gnu@5.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-pXMINibAeruglni8ZajlXEefZHDs7QFSG+vPtkBDu7uiIMpNU8aoktgO2vP+PbIRFD0vHkqTMb64kDtIOqQcwQ=="],
|
|
||||||
|
|
||||||
"cpd-linux-x64-gnu": ["cpd-linux-x64-gnu@5.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-rQ7DuF0lH1HLzjGxlE0aEP2ycfhXgZH/CLSeS7FXNJ38lRVp+iXkrlcrrY6mC4WW/NgbL+DkF7/0lv3tFvGmvg=="],
|
|
||||||
|
|
||||||
"cpd-linux-x64-musl": ["cpd-linux-x64-musl@5.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-Yh+7Go5+fA++I5ssAZg7gUkDCT5CxnzCPvrspbwDrfnwaY6nNM5g1C6Vs0+GJhsspuAKwydJl4nf7jkxzMwRQw=="],
|
|
||||||
|
|
||||||
"cpd-windows-x64-msvc": ["cpd-windows-x64-msvc@5.0.11", "", { "os": "win32", "cpu": "x64" }, "sha512-uV6w85qdfE0WJsrLcGw9A4Kv9ovSnlXZCybMK0esvuiJ7clgaZmDiDPozt5PvrOOShkK/NxzTZSORBSdVnquHA=="],
|
|
||||||
|
|
||||||
"jscpd": ["jscpd@5.0.11", "", { "optionalDependencies": { "cpd-darwin-arm64": "5.0.11", "cpd-darwin-x64": "5.0.11", "cpd-linux-arm64-gnu": "5.0.11", "cpd-linux-x64-gnu": "5.0.11", "cpd-linux-x64-musl": "5.0.11", "cpd-windows-x64-msvc": "5.0.11" }, "bin": { "jscpd": "run-jscpd.js" } }, "sha512-NfLrFJHRM6rIf3oVcdZ4sfhMVop1qxi5r8aC99lpj55YC8hiWaN4VmzU2wcXTwoAo+NS4npXPs9EVEQJ6jyRlg=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,10 +18,47 @@ fn unexpected_response_type_error(
|
||||||
|
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub server_cert: Option<Vec<u8>>,
|
pub tls: ClientTlsConfig,
|
||||||
pub client_id: u64,
|
pub client_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ClientTlsConfig {
|
||||||
|
SystemRoots,
|
||||||
|
PinnedPem(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientConfig {
|
||||||
|
pub fn new(url: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
url: url.into(),
|
||||||
|
tls: ClientTlsConfig::SystemRoots,
|
||||||
|
client_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_tls(mut self, tls: ClientTlsConfig) -> Self {
|
||||||
|
self.tls = tls;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_pinned_pem(self, cert_pem: Vec<u8>) -> Self {
|
||||||
|
self.with_tls(ClientTlsConfig::PinnedPem(cert_pem))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_client_id(mut self, client_id: u64) -> Self {
|
||||||
|
self.client_id = client_id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_cert(&self) -> Option<Vec<u8>> {
|
||||||
|
match &self.tls {
|
||||||
|
ClientTlsConfig::SystemRoots => None,
|
||||||
|
ClientTlsConfig::PinnedPem(cert) => Some(cert.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Established MTP connection with a single negotiated version. */
|
/* Established MTP connection with a single negotiated version. */
|
||||||
pub struct MTPConnection {
|
pub struct MTPConnection {
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
|
|
@ -52,7 +89,7 @@ impl MTPClient {
|
||||||
*/
|
*/
|
||||||
pub async fn connect(config: ClientConfig) -> Result<MTPConnection, CommunicationError> {
|
pub async fn connect(config: ClientConfig) -> Result<MTPConnection, CommunicationError> {
|
||||||
let (sender, receiver) =
|
let (sender, receiver) =
|
||||||
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
|
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||||
|
|
||||||
// Build the initial identification message with the protocol version.
|
// Build the initial identification message with the protocol version.
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
|
|
@ -264,7 +301,7 @@ impl MTPClient {
|
||||||
use mtp_crypto::auth;
|
use mtp_crypto::auth;
|
||||||
|
|
||||||
let (sender, receiver) =
|
let (sender, receiver) =
|
||||||
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
|
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||||
|
|
||||||
let tm = mtp_codec::TypeMap::latest();
|
let tm = mtp_codec::TypeMap::latest();
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
|
|
@ -337,7 +374,7 @@ impl MTPClient {
|
||||||
use mtp_crypto::auth;
|
use mtp_crypto::auth;
|
||||||
|
|
||||||
let (sender, receiver) =
|
let (sender, receiver) =
|
||||||
mtp_transport::connect(&config.url, config.server_cert, Policy::default()).await?;
|
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||||
|
|
||||||
let tm = mtp_codec::TypeMap::latest();
|
let tm = mtp_codec::TypeMap::latest();
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
|
|
@ -414,23 +451,20 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_config_url() {
|
fn test_client_config_url() {
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://example.com:4433");
|
||||||
url: "https://example.com:4433".into(),
|
|
||||||
server_cert: None,
|
|
||||||
client_id: 0,
|
|
||||||
};
|
|
||||||
assert_eq!(config.url, "https://example.com:4433");
|
assert_eq!(config.url, "https://example.com:4433");
|
||||||
assert!(config.server_cert.is_none());
|
assert_eq!(config.tls, ClientTlsConfig::SystemRoots);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_config_with_cert() {
|
fn test_client_config_with_cert() {
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://localhost:4433")
|
||||||
url: "https://localhost:4433".into(),
|
.with_pinned_pem(vec![0x01, 0x02, 0x03])
|
||||||
server_cert: Some(vec![0x01, 0x02, 0x03]),
|
.with_client_id(42);
|
||||||
client_id: 42,
|
assert_eq!(
|
||||||
};
|
config.tls,
|
||||||
assert_eq!(config.server_cert, Some(vec![0x01, 0x02, 0x03]));
|
ClientTlsConfig::PinnedPem(vec![0x01, 0x02, 0x03])
|
||||||
|
);
|
||||||
assert_eq!(config.client_id, 42);
|
assert_eq!(config.client_id, 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,10 @@ impl CommunicationValue {
|
||||||
pub fn get_data(&self, data_type: DataTypeId) -> &DataValue {
|
pub fn get_data(&self, data_type: DataTypeId) -> &DataValue {
|
||||||
self.data.get(&data_type).unwrap_or(&DataValue::Null)
|
self.data.get(&data_type).unwrap_or(&DataValue::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn data(&self) -> &BTreeMap<DataTypeId, DataValue> {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunicationValue {
|
impl CommunicationValue {
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,17 @@ The host creates a QUIC server, manages the registry, and handles version negoti
|
||||||
|
|
||||||
### Initialization
|
### Initialization
|
||||||
|
|
||||||
The host binds to the address from the `mtp_BIND` environment variable (defaults to `::`) on the specified port:
|
The host binds to the address and port supplied in `HostConfig`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::host::{MTPHost, HostConfig};
|
use mtp::host::{HostConfig, MTPHost};
|
||||||
|
|
||||||
let config = HostConfig {
|
let config = HostConfig::new(
|
||||||
port: 4433,
|
"0.0.0.0".parse()?,
|
||||||
tls_fullchain: std::fs::read("cert.pem")?,
|
4433,
|
||||||
tls_key: std::fs::read("key.pem")?,
|
std::fs::read("cert.pem")?,
|
||||||
};
|
std::fs::read("key.pem")?,
|
||||||
|
);
|
||||||
|
|
||||||
let mut host = MTPHost::new(config).await?;
|
let mut host = MTPHost::new(config).await?;
|
||||||
```
|
```
|
||||||
|
|
@ -50,7 +51,7 @@ let mut host = MTPHost::new(config).await?;
|
||||||
### Accepting Connections with Version Negotiation
|
### Accepting Connections with Version Negotiation
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
while let Some(conn) = host.accept().await {
|
while let Some(conn) = host.accept().await? {
|
||||||
// conn.version is the negotiated version
|
// conn.version is the negotiated version
|
||||||
// conn.codec is a VersionedCodec scoped to that version
|
// conn.codec is a VersionedCodec scoped to that version
|
||||||
// conn.sender / conn.receiver for raw CommunicationValue I/O
|
// conn.sender / conn.receiver for raw CommunicationValue I/O
|
||||||
|
|
@ -63,23 +64,23 @@ The host's `accept()` method:
|
||||||
1. Accepts a QUIC connection
|
1. Accepts a QUIC connection
|
||||||
2. If authentication is required (crypto feature): performs login/register handshake
|
2. If authentication is required (crypto feature): performs login/register handshake
|
||||||
3. Reads the first `CommunicationValue` (always encoded with reserved type IDs)
|
3. Reads the first `CommunicationValue` (always encoded with reserved type IDs)
|
||||||
4. Extracts the client's protocol version from `DataType::Version` (wire ID 3)
|
4. Extracts the client's protocol version from `DataType::Version` (reserved data type ID 0)
|
||||||
5. Calls `registry.negotiate(&[client_version])`
|
5. Calls `registry.negotiate(&[client_version])`
|
||||||
6. Returns `None` if the version is unsupported
|
6. Returns an `AcceptError` if the version is unsupported
|
||||||
7. Returns an `MTPConnection` with the negotiated version otherwise
|
7. Returns `Ok(Some(MTPConnection))` with the negotiated version otherwise
|
||||||
|
|
||||||
### Login/Register Handshake
|
### Login/Register Handshake
|
||||||
|
|
||||||
When `require_authentication` is set, the parties run a mutually-authenticated
|
When `require_authentication` is set, the parties run a mutually-authenticated
|
||||||
**challenge-response**. The client speaks first with an *unsigned* hello:
|
**challenge-response**. The client speaks first with an *unsigned* hello:
|
||||||
|
|
||||||
- **Login** (`CommunicationType::Identification`, ID 15): version, client ID
|
- **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID
|
||||||
- **Register** (`CommunicationType::Register`, ID 17): version, public keys
|
- **Register** (`CommunicationType::Register`, reserved ID 2): version, public keys
|
||||||
|
|
||||||
The host then issues a fresh random `server_challenge` in a signed `Challenge`
|
The host then issues a fresh random `server_challenge` in a signed `Challenge`
|
||||||
(`CommunicationType::Challenge`, ID 21, carrying `ServerNonce`). The client signs
|
(`CommunicationType::Challenge`, reserved ID 4, carrying `ServerNonce`). The client signs
|
||||||
that challenge, binding its id (login) or public keys (register), and returns a
|
that challenge, binding its id (login) or public keys (register), and returns a
|
||||||
`ChallengeResponse` (ID 22). The host verifies the proof against the challenge it
|
`ChallengeResponse` (reserved ID 5). The host verifies the proof against the challenge it
|
||||||
issued and sends a signed final response, which the client verifies.
|
issued and sends a signed final response, which the client verifies.
|
||||||
|
|
||||||
Because the client's proof covers the host-issued `server_challenge` (a one-time
|
Because the client's proof covers the host-issued `server_challenge` (a one-time
|
||||||
|
|
@ -94,21 +95,19 @@ replayed on another connection. All signed payloads are domain-separated; see
|
||||||
The client connects to a host and uses a single compiled-in protocol version.
|
The client connects to a host and uses a single compiled-in protocol version.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::client::{MTPClient, ClientConfig};
|
use mtp::client::{ClientConfig, MTPClient};
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://host.example.com:4433");
|
||||||
url: "https://host.example.com:4433".into(),
|
let pinned = config.clone().with_pinned_pem(cert_pem_bytes);
|
||||||
server_cert: None, // or Some(cert_pem_bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect (unauthenticated, existing client)
|
// Connect (unauthenticated, existing client)
|
||||||
let conn = MTPClient::connect(config, 8765).await?;
|
let conn = MTPClient::connect(config.clone().with_client_id(8765)).await?;
|
||||||
|
|
||||||
// Authenticated login
|
// Authenticated login
|
||||||
let conn = MTPClient::auth_connect(config, 8765, keys, host_pk).await?;
|
let conn = MTPClient::auth_connect(pinned.with_client_id(8765), &keys, &host_pk).await?;
|
||||||
|
|
||||||
// Registration (new client)
|
// Registration (new client)
|
||||||
let conn = MTPClient::auth_register(config, keys, host_pk).await?;
|
let conn = MTPClient::auth_register(config, &keys, &host_pk).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
The client's `PROTOCOL_VERSION` constant is set by `protocol_version` in `type-maps.yaml` and baked in at compile time. The client never imports the `registry` crate; it only uses `mtp::type_map` for enum types and `mtp::codec` for encoding.
|
The client's `PROTOCOL_VERSION` constant is set by `protocol_version` in `type-maps.yaml` and baked in at compile time. The client never imports the `registry` crate; it only uses `mtp::type_map` for enum types and `mtp::codec` for encoding.
|
||||||
|
|
|
||||||
|
|
@ -17,24 +17,22 @@ mtp = { path = "/path/to/mtp", features = ["client", "crypto"] }
|
||||||
## ClientConfig
|
## ClientConfig
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::client::ClientConfig;
|
use mtp::client::{ClientConfig, ClientTlsConfig};
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://host.example.com:4433")
|
||||||
url: "https://host.example.com:4433".into(),
|
.with_tls(ClientTlsConfig::SystemRoots)
|
||||||
server_cert: None, // None = use system root certificates
|
.with_client_id(0);
|
||||||
client_id: 0, // previously assigned ID or 0
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|--------------|--------------------|-----------------------------------------------------|
|
|--------------|--------------------|-----------------------------------------------------|
|
||||||
| `url` | `String` | `https://host:port` address of the MTP host |
|
| `url` | `String` | `https://host:port` address of the MTP host |
|
||||||
| `server_cert`| `Option<Vec<u8>>` | `None` to use system roots, `Some(pem_bytes)` to pin |
|
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
|
||||||
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
||||||
|
|
||||||
### TLS Certificate Handling
|
### TLS Certificate Handling
|
||||||
|
|
||||||
When `server_cert` is `None` (the default), the client loads the **system's
|
When `tls` is `ClientTlsConfig::SystemRoots` (the default), the client loads the **system's
|
||||||
native root certificate store** via `rustls_native_certs`. This works with
|
native root certificate store** via `rustls_native_certs`. This works with
|
||||||
publicly-trusted CAs out of the box on Linux (using `openssl-probe`), macOS
|
publicly-trusted CAs out of the box on Linux (using `openssl-probe`), macOS
|
||||||
(Keychain), and Windows (Root Store).
|
(Keychain), and Windows (Root Store).
|
||||||
|
|
@ -44,10 +42,7 @@ certificates:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let pem = std::fs::read("my-server-cert.pem")?;
|
let pem = std::fs::read("my-server-cert.pem")?;
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://host.example.com:4433").with_pinned_pem(pem);
|
||||||
server_cert: Some(pem),
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When pinned, **only** the given certificate(s) are trusted for the TLS
|
When pinned, **only** the given certificate(s) are trusted for the TLS
|
||||||
|
|
@ -78,13 +73,9 @@ pub struct MTPConnection {
|
||||||
### Unauthenticated Connect
|
### Unauthenticated Connect
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::client::{MTPClient, ClientConfig};
|
use mtp::client::{ClientConfig, MTPClient};
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://host.example.com:4433").with_client_id(42);
|
||||||
url: "https://host.example.com:4433".into(),
|
|
||||||
server_cert: None,
|
|
||||||
client_id: 42,
|
|
||||||
};
|
|
||||||
|
|
||||||
let conn = MTPClient::connect(config).await?;
|
let conn = MTPClient::connect(config).await?;
|
||||||
```
|
```
|
||||||
|
|
@ -101,10 +92,8 @@ use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||||
let keys = Keyring::from_bytes(&saved_keyring_bytes)?;
|
let keys = Keyring::from_bytes(&saved_keyring_bytes)?;
|
||||||
let host_pk = PublicKeyBundle::from_bytes(&saved_host_pk_bytes)?;
|
let host_pk = PublicKeyBundle::from_bytes(&saved_host_pk_bytes)?;
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://host.example.com:4433")
|
||||||
client_id: 42, // must match the keyring's identity
|
.with_client_id(42); // must match the keyring's identity
|
||||||
// ...
|
|
||||||
};
|
|
||||||
|
|
||||||
let conn = MTPClient::auth_connect(config, &keys, &host_pk).await?;
|
let conn = MTPClient::auth_connect(config, &keys, &host_pk).await?;
|
||||||
```
|
```
|
||||||
|
|
@ -315,7 +304,11 @@ using `MTPClient`:
|
||||||
```rust
|
```rust
|
||||||
use mtp_transport::{connect, Policy};
|
use mtp_transport::{connect, Policy};
|
||||||
|
|
||||||
let (sender, receiver) = connect(&config.url, config.server_cert, policy).await?;
|
let server_cert = match &config.tls {
|
||||||
|
ClientTlsConfig::SystemRoots => None,
|
||||||
|
ClientTlsConfig::PinnedPem(pem) => Some(pem.clone()),
|
||||||
|
};
|
||||||
|
let (sender, receiver) = connect(&config.url, server_cert, policy).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build and send the initial `Identification` frame manually to complete
|
Then build and send the initial `Identification` frame manually to complete
|
||||||
|
|
@ -324,7 +317,7 @@ version negotiation.
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
The client's protocol version is baked in at compile time via the
|
The client's protocol version is baked in at compile time via the
|
||||||
`PROTOCOL_VERSION` constant from `mtp_codec`. The version is set by the
|
`PROTOCOL_VERSION` constant from `mtp::codec`. The version is set by the
|
||||||
`protocol_version` field in your `type-maps.yaml`.
|
`protocol_version` field in your `type-maps.yaml`.
|
||||||
|
|
||||||
The client never imports the `registry` module; it uses a single compiled-in
|
The client never imports the `registry` module; it uses a single compiled-in
|
||||||
|
|
|
||||||
|
|
@ -21,25 +21,23 @@ mtp = { path = "/path/to/mtp", features = ["host", "crypto"] }
|
||||||
use mtp::host::HostConfig;
|
use mtp::host::HostConfig;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
let config = HostConfig {
|
let config = HostConfig::new(
|
||||||
ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||||
port: 4433,
|
4433,
|
||||||
tls_fullchain: std::fs::read("cert.pem")?,
|
std::fs::read("cert.pem")?,
|
||||||
tls_key: std::fs::read("key.pem")?,
|
std::fs::read("key.pem")?,
|
||||||
|
)
|
||||||
// Crypto fields (required when feature = "crypto"):
|
.with_authentication(
|
||||||
require_authentication: true,
|
/* Keyring */,
|
||||||
host_id: 1,
|
|client_id: u64| -> Option<PublicKeyBundle> {
|
||||||
host_keyring: /* Keyring */,
|
|
||||||
get_existing_user: Box::new(|client_id: u64| -> Option<PublicKeyBundle> {
|
|
||||||
CLIENT_DB.lock().unwrap().get(&client_id).cloned()
|
CLIENT_DB.lock().unwrap().get(&client_id).cloned()
|
||||||
}),
|
},
|
||||||
complete_register: Box::new(|bundle: PublicKeyBundle| -> u64 {
|
|bundle: PublicKeyBundle| -> u64 {
|
||||||
let id = next_id();
|
let id = next_id();
|
||||||
CLIENT_DB.lock().unwrap().insert(id, bundle);
|
CLIENT_DB.lock().unwrap().insert(id, bundle);
|
||||||
id
|
id
|
||||||
}),
|
},
|
||||||
};
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|
|
@ -49,10 +47,9 @@ let config = HostConfig {
|
||||||
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
|
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
|
||||||
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
|
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
|
||||||
| `require_authentication` | `bool` (crypto) | Enable login/register handshake |
|
| `require_authentication` | `bool` (crypto) | Enable login/register handshake |
|
||||||
| `host_id` | `u64` (crypto) | Host identifier |
|
|
||||||
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
||||||
| `get_existing_user` | `Box<dyn Fn(u64) -> Option<PublicKeyBundle> + Send>` (crypto) | Lookup callback for login |
|
| `get_existing_user` | `Fn(u64) -> Option<PublicKeyBundle> + Send + Sync` (crypto) | Lookup callback for login |
|
||||||
| `complete_register` | `Box<dyn Fn(PublicKeyBundle) -> u64 + Send>` (crypto) | Registration callback, returns new client ID |
|
| `complete_register` | `Fn(PublicKeyBundle) -> u64 + Send + Sync` (crypto) | Registration callback, returns new client ID |
|
||||||
|
|
||||||
### TLS
|
### TLS
|
||||||
|
|
||||||
|
|
@ -67,7 +64,7 @@ use mtp::host::MTPHost;
|
||||||
let mut host = MTPHost::new(config).await?;
|
let mut host = MTPHost::new(config).await?;
|
||||||
println!("Listening on {}", host.local_addr());
|
println!("Listening on {}", host.local_addr());
|
||||||
|
|
||||||
while let Some(conn) = host.accept().await {
|
while let Some(conn) = host.accept().await? {
|
||||||
// conn is an MTPConnection ready for I/O
|
// conn is an MTPConnection ready for I/O
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -107,12 +104,12 @@ When a client connects, `accept()` performs the following sequence:
|
||||||
1. Accept the QUIC connection
|
1. Accept the QUIC connection
|
||||||
2. Read the client's first `CommunicationValue` (always encoded with reserved
|
2. Read the client's first `CommunicationValue` (always encoded with reserved
|
||||||
type IDs)
|
type IDs)
|
||||||
3. Extract the protocol version from `DataType::Version` (wire ID 3) as a
|
3. Extract the protocol version from `DataType::Version` (reserved data type ID 0) as a
|
||||||
`DataValue::Str("major.minor")`
|
`DataValue::Str("major.minor")`
|
||||||
4. Call `registry.negotiate(&[client_version])` to find the highest mutually
|
4. Call `registry.negotiate(&[client_version])` to find the highest mutually
|
||||||
supported version
|
supported version
|
||||||
5. Return `None` (closing the connection) if no compatible version exists
|
5. Return an `AcceptError` (closing the connection) if no compatible version exists
|
||||||
6. Return an `MTPConnection` with the negotiated version
|
6. Return `Ok(Some(MTPConnection))` with the negotiated version
|
||||||
|
|
||||||
The `Registry` is built automatically from all type maps defined in your
|
The `Registry` is built automatically from all type maps defined in your
|
||||||
`type-maps.yaml` via `Registry::builtin()`.
|
`type-maps.yaml` via `Registry::builtin()`.
|
||||||
|
|
@ -221,14 +218,14 @@ available for verifying subsequent signed messages from the client.
|
||||||
|
|
||||||
If verification fails or the client is not found (login), the host sends a
|
If verification fails or the client is not found (login), the host sends a
|
||||||
rejection response with `Connected=false` and closes the send stream, returning
|
rejection response with `Connected=false` and closes the send stream, returning
|
||||||
`None` from `accept()`.
|
`AcceptError::AuthenticationFailed` from `accept()`.
|
||||||
|
|
||||||
## Handling Messages
|
## Handling Messages
|
||||||
|
|
||||||
Use `conn.sender` and `conn.receiver` for bidirectional message exchange:
|
Use `conn.sender` and `conn.receiver` for bidirectional message exchange:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
while let Some(conn) = host.accept().await {
|
while let Some(conn) = host.accept().await? {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match conn.receiver.receive().await {
|
match conn.receiver.receive().await {
|
||||||
|
|
@ -265,9 +262,9 @@ verification. Must return `Some(PublicKeyBundle)` if the client ID is known,
|
||||||
or `None` to reject.
|
or `None` to reject.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let get_existing_user = Box::new(|id: u64| -> Option<PublicKeyBundle> {
|
let get_existing_user = |id: u64| -> Option<PublicKeyBundle> {
|
||||||
db.lock().unwrap().get(&id).cloned()
|
db.lock().unwrap().get(&id).cloned()
|
||||||
});
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### complete_register
|
### complete_register
|
||||||
|
|
@ -277,16 +274,16 @@ assign a client ID. The returned `u64` becomes the client's permanent
|
||||||
identifier.
|
identifier.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let complete_register = Box::new(|bundle: PublicKeyBundle| -> u64 {
|
let complete_register = |bundle: PublicKeyBundle| -> u64 {
|
||||||
let mut db = db.lock().unwrap();
|
let mut db = db.lock().unwrap();
|
||||||
let id = next_id;
|
let id = next_id;
|
||||||
next_id += 1;
|
next_id += 1;
|
||||||
db.insert(id, bundle);
|
db.insert(id, bundle);
|
||||||
id
|
id
|
||||||
});
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Both callbacks are called from within `accept()` and must be `Send`. They are
|
Both callbacks are called from within `accept()` and must be `Send + Sync`. They are
|
||||||
invoked synchronously, so avoid long-running operations (or use `spawn_blocking`
|
invoked synchronously, so avoid long-running operations (or use `spawn_blocking`
|
||||||
if needed, though the callbacks are `Fn`, not `AsyncFn`).
|
if needed, though the callbacks are `Fn`, not `AsyncFn`).
|
||||||
|
|
||||||
|
|
@ -338,5 +335,3 @@ let transport = host(ip, port, cert, key, custom_policy).await?;
|
||||||
|
|
||||||
Drop the `MTPHost` to stop accepting new connections. Active connections
|
Drop the `MTPHost` to stop accepting new connections. Active connections
|
||||||
continue until their `Sender`/`Receiver` are dropped or the peer disconnects.
|
continue until their `Sender`/`Receiver` are dropped or the peer disconnects.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,18 @@ Type names are defined in a YAML config and turned into Rust enums at **compile
|
||||||
|
|
||||||
### Defining Type Maps
|
### Defining Type Maps
|
||||||
|
|
||||||
An example `type-maps.yaml` is provided in the [`example-type-maps.yaml`](./example-type-maps.yaml) file. Place your own `type-maps.yaml` in your project root and set the `MTP_TYPE_MAPS` environment variable (see [Customizing Type Maps in Downstream Projects](#customizing-type-maps-in-downstream-projects)).
|
Place your own `type-maps.yaml` in your project root. Browser apps should use the Vite plugin so the app-specific WASM package is generated into Vite's cache during dev/build:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [mtp({ typeMaps: "./type-maps.yaml" })],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rust and manual WASM builds can set `MTP_TYPE_MAPS` directly (see [Customizing Type Maps in Downstream Projects](#customizing-type-maps-in-downstream-projects)).
|
||||||
|
|
||||||
### Using Generated Enums
|
### Using Generated Enums
|
||||||
|
|
||||||
|
|
@ -84,7 +95,18 @@ let decoded = codec.decode(&bytes, Version(2, 0)).unwrap();
|
||||||
|
|
||||||
## Customizing Type Maps in Downstream Projects
|
## Customizing Type Maps in Downstream Projects
|
||||||
|
|
||||||
External projects must provide their own type map configuration via the `MTP_TYPE_MAPS` environment variable. There is no bundled default; the build script will error if the variable is not set or points to an invalid file.
|
External projects must provide their own type map configuration. Browser projects should install `mtp` and configure `mtp/vite`; they do not need to publish, fork, or copy a generated WASM package.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [mtp({ typeMaps: "./type-maps.yaml" })],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For Rust builds, or when invoking `wasm-pack` manually, set the `MTP_TYPE_MAPS` environment variable. If the variable points to an invalid file, the build fails. If `MTP_TYPE_MAPS` is not set, the build script emits a warning and generates reserved protocol types only; application-specific communication and data types will not be available.
|
||||||
|
|
||||||
1. Create a `type-maps.yaml` in your project root
|
1. Create a `type-maps.yaml` in your project root
|
||||||
2. Set the `MTP_TYPE_MAPS` environment variable in `.cargo/config.toml`:
|
2. Set the `MTP_TYPE_MAPS` environment variable in `.cargo/config.toml`:
|
||||||
|
|
|
||||||
|
|
@ -1,291 +1,282 @@
|
||||||
# MTP WASM Client
|
# MTP WASM Client
|
||||||
|
|
||||||
The WASM client is a browser-compatible MTP implementation that uses the **WebTransport** API to communicate with an MTP host over QUIC (HTTP/3). It compiles from Rust to WebAssembly via `wasm-bindgen` and exposes a JavaScript/TypeScript API through the `mtp-wasm` npm package.
|
The browser client is exposed through the `mtp` npm package. Most applications should use the SDK-first `MTPClient` API; direct generated WASM bindings remain available from `mtp/raw` for advanced integrations.
|
||||||
|
|
||||||
## Package
|
## Package Entry Points
|
||||||
|
|
||||||
The compiled package lives in `wasm/pkg/` and contains:
|
|
||||||
|
|
||||||
- `mtp_wasm.js` -- generated JS glue
|
|
||||||
- `mtp_wasm_bg.wasm` -- the WebAssembly binary
|
|
||||||
- `mtp_wasm.d.ts` -- TypeScript type declarations
|
|
||||||
- `package.json` -- npm package definition
|
|
||||||
|
|
||||||
Install or copy these files into your web project. Then initialise the module:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import init, { WasmClient } from 'mtp-wasm';
|
import { MTPClient } from "mtp";
|
||||||
|
import init, { WasmClient } from "mtp/raw";
|
||||||
await init();
|
import { mtp } from "mtp/vite";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Browser Support
|
- `mtp` exports the SDK-first `MTPClient` wrapper.
|
||||||
|
- `mtp/raw` exports the generated `wasm-bindgen` module and raw classes/functions.
|
||||||
|
- `mtp/vite` exports the Vite plugin that builds app-specific WASM bindings from your `type-maps.yaml`.
|
||||||
|
- `mtp/type-map` exports generated TypeScript unions for communication and data type names.
|
||||||
|
|
||||||
WebTransport is required. Check availability at runtime:
|
## Vite Type-Map Workflow
|
||||||
|
|
||||||
|
Browser apps provide their own type map. The Vite plugin runs `wasm-pack` during dev/build with `MTP_TYPE_MAPS` set, writes generated output under `node_modules/.vite/mtp/` by default, and aliases `mtp/raw` plus `mtp/type-map` to that generated output.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (!WasmClient.is_supported()) {
|
// vite.config.ts
|
||||||
// fall back or show an error
|
import { defineConfig } from "vite";
|
||||||
}
|
import { mtp } from "mtp/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [mtp({ typeMaps: "./type-maps.yaml" })],
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connecting to a Host
|
You do not need to publish, fork, or copy an app-specific generated WASM package.
|
||||||
|
|
||||||
### ConnectionConfig
|
## SDK Quick Start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const config = new ConnectionConfig("https://host.example.com:4433");
|
import { MTPClient } from "mtp";
|
||||||
config.client_id = 12345n; // optional, for re-authentication
|
|
||||||
config.server_certificate_hashes = [ // optional, for certificate pinning
|
|
||||||
"sha-256:abc123...",
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
`client_id` is only needed for authenticated login (`auth_connect`). For registration (`auth_register`) it is ignored.
|
const client = await MTPClient.create({
|
||||||
|
url: "https://host.example.com:4433",
|
||||||
|
hostPublicKey,
|
||||||
|
credentials,
|
||||||
|
storage,
|
||||||
|
serverCertificateHashes: ["sha-256:abcd1234..."],
|
||||||
|
pings: true,
|
||||||
|
logger: (event) => console.log(event),
|
||||||
|
});
|
||||||
|
|
||||||
### TLS Certificate Handling
|
const unsubscribe = client.subscribe("SomeType", (message) => {
|
||||||
|
console.log(message.type, message.data);
|
||||||
|
});
|
||||||
|
|
||||||
By default, when `server_certificate_hashes` is not set, the browser uses its
|
await client.connectOrRegister();
|
||||||
**built-in root certificate store** to verify the server's TLS certificate,
|
await client.send("SomeType", { value: "hello" });
|
||||||
just like any other HTTPS/WebSocket connection. This works with publicly-trusted
|
|
||||||
certificate authorities automatically.
|
|
||||||
|
|
||||||
For development or self-signed certificates, pin the server certificate by
|
const response = await client.request(
|
||||||
providing its hash:
|
"SomeRequestType",
|
||||||
|
{ id: "abc" },
|
||||||
```typescript
|
{ responseType: "SomeResponseType" },
|
||||||
config.server_certificate_hashes = [
|
|
||||||
"sha-256:abcd1234...", // hex-encoded hash value
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
The hash format is `"<algorithm>:<hex-encoded-hash>"`, where the only algorithm
|
|
||||||
the browser's WebTransport API currently accepts is `sha-256`. When hashes are
|
|
||||||
provided, the browser **only** trusts certificates matching one of the given
|
|
||||||
hashes and ignores its root store for this connection.
|
|
||||||
|
|
||||||
### Callbacks
|
|
||||||
|
|
||||||
The client uses three callbacks for state, messages, and errors:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const client = new WasmClient(
|
|
||||||
(state: number) => console.log("state", state), // ConnectionState enum
|
|
||||||
(data: Uint8Array) => console.log("msg", data), // raw frame bytes
|
|
||||||
(err: any) => console.error("err", err), // error description
|
|
||||||
);
|
);
|
||||||
```
|
|
||||||
|
|
||||||
### Connection States
|
client.raw.client; // underlying WasmClient instance
|
||||||
|
client.raw.bindings; // generated raw WASM module exports
|
||||||
|
|
||||||
| Value | Name |
|
unsubscribe();
|
||||||
|-------|--------------|
|
|
||||||
| 0 | Disconnected |
|
|
||||||
| 1 | Connecting |
|
|
||||||
| 2 | Connected |
|
|
||||||
| 3 | Failed |
|
|
||||||
|
|
||||||
Poll `client.state` at any time.
|
|
||||||
|
|
||||||
## Connection Methods
|
|
||||||
|
|
||||||
### Unauthenticated Connect
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await client.connect(config);
|
|
||||||
```
|
|
||||||
|
|
||||||
Sends an `Identification` frame with the protocol version and client ID. The host may accept or reject. No cryptographic handshake occurs.
|
|
||||||
|
|
||||||
### Authenticated Login
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const confirmedId = await client.auth_connect(
|
|
||||||
config,
|
|
||||||
hostPublicKeyBytes, // Uint8Array: serialized PublicKeyBundle from the host
|
|
||||||
keyringBytes, // Uint8Array: serialized Keyring matching the client ID
|
|
||||||
clientId, // bigint: previously assigned client ID
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Exchange (challenge-response): the client sends an unsigned `Identification`
|
|
||||||
hello, the host replies with a signed `Challenge` carrying a fresh
|
|
||||||
`server_challenge`, the client signs that challenge in a `ChallengeResponse`, and
|
|
||||||
the host verifies it and replies with a signed `IdentificationResponse`. Signing
|
|
||||||
over the host-issued challenge is what prevents a captured proof from being
|
|
||||||
replayed on another connection. Returns the confirmed client ID.
|
|
||||||
|
|
||||||
### Registration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const newId = await client.auth_register(
|
|
||||||
config,
|
|
||||||
hostPublicKeyBytes, // Uint8Array: serialized PublicKeyBundle from the host
|
|
||||||
keyringBytes, // Uint8Array: serialized Keyring for the new identity
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Exchange (challenge-response): the client sends an unsigned `Register` hello with
|
|
||||||
its public keys, the host replies with a signed `Challenge`, the client signs it
|
|
||||||
(binding the public-key bundle) in a `ChallengeResponse`, and the host verifies
|
|
||||||
it, assigns a new ID, and responds with a signed `RegisterResponse`. Returns the
|
|
||||||
newly assigned client ID.
|
|
||||||
|
|
||||||
## Sending and Receiving Messages
|
|
||||||
|
|
||||||
### Send
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const frame = build_ping_frame(clientId, "hello", timestamp, data);
|
|
||||||
await client.send(frame);
|
|
||||||
```
|
|
||||||
|
|
||||||
`send()` takes raw frame bytes (a serialized `CommunicationValue`). Build frames
|
|
||||||
with the provided helper functions or construct them manually.
|
|
||||||
|
|
||||||
### Receive
|
|
||||||
|
|
||||||
Incoming frames arrive on the `on_message` callback registered in the constructor.
|
|
||||||
The callback receives a `Uint8Array` of raw frame bytes. Parse with
|
|
||||||
`CommunicationValue.from_bytes()` on the Rust side or handle the bytes in JS.
|
|
||||||
|
|
||||||
### Disconnect
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
```
|
```
|
||||||
|
|
||||||
Gracefully closes the WebTransport session.
|
`MTPClient.isSupported()` checks whether the current browser exposes WebTransport:
|
||||||
|
|
||||||
## Building Frames
|
|
||||||
|
|
||||||
### `build_ping_frame`
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function build_ping_frame(
|
if (!MTPClient.isSupported()) {
|
||||||
clientId: bigint,
|
throw new Error("WebTransport is not available in this browser");
|
||||||
description: string,
|
|
||||||
timestamp: bigint,
|
|
||||||
data: Uint8Array,
|
|
||||||
): Uint8Array;
|
|
||||||
```
|
|
||||||
|
|
||||||
Constructs a basic `Ping` message with description, timestamp, and optional
|
|
||||||
binary payload. Useful for health checks and simple messaging.
|
|
||||||
|
|
||||||
### `build_demo_message`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function build_demo_message(
|
|
||||||
clientId: bigint,
|
|
||||||
keyringBytes: Uint8Array,
|
|
||||||
hostBundleBytes: Uint8Array,
|
|
||||||
): Uint8Array;
|
|
||||||
```
|
|
||||||
|
|
||||||
Constructs a `Ping` frame that demonstrates encrypted, signed, and
|
|
||||||
signed+encrypted containers. The containers are ML-KEM-encrypted to the host's
|
|
||||||
`PublicKeyBundle` (`hostBundleBytes`, the same bytes passed to `auth_connect` /
|
|
||||||
`auth_register`), so the host decrypts them with its own keyring; signatures use
|
|
||||||
the client keyring's Ed25519 key. The client keyring only needs its Ed25519
|
|
||||||
signing key for this demo.
|
|
||||||
|
|
||||||
### `parse_auth_response`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function parse_auth_response(response: Uint8Array): any;
|
|
||||||
```
|
|
||||||
|
|
||||||
Parses an `IdentificationResponse` or `RegisterResponse` frame into a JS object:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
connected: boolean,
|
|
||||||
clientNonce?: Uint8Array,
|
|
||||||
assignedId?: number,
|
|
||||||
timestamp?: number,
|
|
||||||
signature?: Uint8Array,
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Crypto Primitives
|
## Credentials And Storage
|
||||||
|
|
||||||
### Key Generation
|
Authenticated connections need stable key material. Pass `credentials` when you already have a client ID and serialized keyring, or pass a small `storage` object and let the SDK persist credentials after registration.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = ed25519_generate();
|
const storage = {
|
||||||
// result.signer -> WasmEd25519Signer
|
getItem: (key: string) => localStorage.getItem(key),
|
||||||
// result.secretKey -> Uint8Array (32 bytes)
|
setItem: (key: string, value: string) => localStorage.setItem(key, value),
|
||||||
// result.publicKey -> Uint8Array (32 bytes)
|
removeItem: (key: string) => localStorage.removeItem(key),
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await MTPClient.create({
|
||||||
|
url: "https://host.example.com:4433",
|
||||||
|
hostPublicKey,
|
||||||
|
storage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientId = await client.connectOrRegister();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Keyring
|
The storage contract is intentionally small and may be sync or async:
|
||||||
|
|
||||||
A `Keyring` bundles all key material for an identity. For Ed25519-only setups:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const keyringBytes = keyring_from_ed25519(secretKey, publicKey);
|
interface MTPCredentialStorage {
|
||||||
// keyringBytes is ready for WasmClient.auth_register or WasmClient.auth_connect
|
getItem(key: string): string | null | Promise<string | null>;
|
||||||
|
setItem(key: string, value: string): void | Promise<void>;
|
||||||
|
removeItem(key: string): void | Promise<void>;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Full keyring with KEM + ML-DSA requires constructing on the Rust side. The
|
`credentials` can also be supplied directly:
|
||||||
serialized bytes are portable:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const keyring = WasmKeyring.from_bytes(keyringBytes);
|
const client = await MTPClient.create({
|
||||||
const bundle = keyring.public_key_bundle();
|
url: "https://host.example.com:4433",
|
||||||
// bundle.kem_public_key -> Uint8Array
|
hostPublicKey,
|
||||||
// bundle.sig_cl_public_key -> Uint8Array
|
credentials: {
|
||||||
// bundle.sig_pq_public_key -> Uint8Array
|
clientId: 42n,
|
||||||
|
keyring: savedKeyringBytes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Signing and Verification
|
`client.credentials` returns the current public credential object. Call `clearCredentials()` to remove in-memory credentials and delete the configured storage key.
|
||||||
|
|
||||||
|
## Connection Methods
|
||||||
|
|
||||||
|
- `connect()` opens a connection. If credentials include a `clientId` and `hostPublicKey` is available, it uses authenticated login; otherwise it uses unauthenticated connect.
|
||||||
|
- `register()` performs authenticated registration and persists the assigned client ID when storage is configured.
|
||||||
|
- `connectOrRegister()` registers when no client ID is present, otherwise performs authenticated login.
|
||||||
|
- `disconnect()` stops protocol pings and closes the underlying WebTransport session.
|
||||||
|
|
||||||
|
For certificate pinning, pass WebTransport certificate hashes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const signer = new WasmEd25519Signer(secretKey);
|
await MTPClient.create({
|
||||||
const sig = signer.sign(message); // Uint8Array
|
url: "https://host.example.com:4433",
|
||||||
signer.verify(message, sig); // throws on mismatch
|
serverCertificateHashes: ["sha-256:abcd1234..."],
|
||||||
|
});
|
||||||
// Standalone verification (no signer object needed):
|
|
||||||
ed25519_verify(publicKey, message, signature);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Symmetric Encryption
|
If hashes are omitted, the browser uses its normal TLS root store.
|
||||||
|
|
||||||
|
## Sending, Requests, Subscriptions, And Pings
|
||||||
|
|
||||||
|
`send` accepts either a typed message or a prebuilt raw frame:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const cipher = new WasmChaCha20Poly1305(key); // 32-byte key
|
await client.send("SomeType", { value: "hello" });
|
||||||
const encrypted = cipher.encrypt(plaintext, aad); // nonce || ciphertext
|
await client.send(rawFrameBytes);
|
||||||
const decrypted = cipher.decrypt(encrypted, aad);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hashing and KDF
|
Typed sends are encoded by the generated WASM binding using the app type map. Optional frame metadata can be passed as the third argument:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const hash = wasm_sha256(data); // 32 bytes
|
await client.send("SomeType", { value: "hello" }, {
|
||||||
const double = wasm_sha256_double(data); // SHA-256(SHA-256(data))
|
id: 7,
|
||||||
|
sender: client.credentials?.clientId ?? 0n,
|
||||||
const derived = wasm_hkdf_expand(ikm, salt, info, len);
|
});
|
||||||
const encKey = wasm_derive_encryption_key(ikm, salt, context); // 32 bytes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lifecycle and Best Practices
|
`request` sends one frame and resolves with the matching parsed response from the WASM layer:
|
||||||
|
|
||||||
1. **Key persistence** -- serialise keyring bytes after registration and store
|
```typescript
|
||||||
them (e.g. in `localStorage`). On next visit, load the saved keyring and
|
const response = await client.request(
|
||||||
call `auth_connect` instead of registering again.
|
"SomeRequestType",
|
||||||
|
{ id: "abc" },
|
||||||
|
{ responseType: "SomeResponseType" },
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
2. **Ownership** -- call `config.free()` after connecting if the config object
|
`subscribe` registers a message-type handler and returns an unsubscribe function:
|
||||||
is no longer needed. WASM objects (`WasmClient`, `WasmKeyring`, etc.) are
|
|
||||||
garbage-collected, but explicit `free()` or `dispose()` reclaims memory
|
|
||||||
sooner.
|
|
||||||
|
|
||||||
3. **Receive loop** -- once `connect`, `auth_connect`, or `auth_register`
|
```typescript
|
||||||
resolves, the receive loop is running in the background. Incoming frames
|
const unsubscribe = client.subscribe("SomeType", (message) => {
|
||||||
arrive on the `on_message` callback. There is no need to poll.
|
console.log(message.id, message.sender, message.data);
|
||||||
|
});
|
||||||
|
|
||||||
4. **Single active client** -- a `WasmClient` manages one WebTransport session.
|
unsubscribe();
|
||||||
Create a new instance for each connection.
|
```
|
||||||
|
|
||||||
5. **State transitions** -- after `disconnect()` the client transitions to
|
Protocol pings are real MTP `Ping` frames sent by the WASM client, not just transport keepalives:
|
||||||
`Disconnected`. The instance is reusable; call a connect method again to
|
|
||||||
open a new session.
|
```typescript
|
||||||
|
await MTPClient.create({
|
||||||
|
url: "https://host.example.com:4433",
|
||||||
|
pings: { intervalMs: 30_000 },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `pings: true` for the default interval.
|
||||||
|
|
||||||
|
## Logger Events
|
||||||
|
|
||||||
|
The SDK logger receives parsed events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MTPLogEvent =
|
||||||
|
| { hint: "info" | "warning"; type: string; data: unknown }
|
||||||
|
| { hint: "error"; type: string | "error"; error: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
Incoming non-error frames and sent frames are logged as `info`. Error frames and transport errors are logged as `error`.
|
||||||
|
|
||||||
|
## Advanced Raw Bindings
|
||||||
|
|
||||||
|
Use `mtp/raw` when you need direct access to the generated `wasm-bindgen` API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import init, {
|
||||||
|
ConnectionConfig,
|
||||||
|
WasmClient,
|
||||||
|
ed25519_generate,
|
||||||
|
keyring_from_ed25519,
|
||||||
|
} from "mtp/raw";
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
const rawClient = new WasmClient(
|
||||||
|
(state) => console.log("state", state),
|
||||||
|
(frame) => console.log("message", frame),
|
||||||
|
(error) => console.error(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = new ConnectionConfig("https://host.example.com:4433");
|
||||||
|
config.client_id = 42n;
|
||||||
|
|
||||||
|
await rawClient.connect(config);
|
||||||
|
config.free();
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw callbacks receive parsed frames, not application-specific SDK objects:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ParsedFrame {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
sender?: bigint;
|
||||||
|
receiver?: bigint;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
raw: Uint8Array;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw message helpers that remain available include:
|
||||||
|
|
||||||
|
- `build_frame(messageType, data, options?)`
|
||||||
|
- `build_ping_frame(clientId, description, timestamp, data)`
|
||||||
|
- `parse_frame(frame)`
|
||||||
|
- `format_frame(frame)`
|
||||||
|
- `parse_auth_response(frame)`
|
||||||
|
|
||||||
|
Raw crypto and key helpers include:
|
||||||
|
|
||||||
|
- `ed25519_generate()`
|
||||||
|
- `ed25519_verify(publicKey, message, signature)`
|
||||||
|
- `keyring_from_ed25519(secretKey, publicKey)`
|
||||||
|
- `WasmKeyring.from_bytes(bytes)` and `keyring.to_bytes()`
|
||||||
|
- `WasmPublicKeyBundle.from_bytes(bytes)` and `bundle.to_bytes()`
|
||||||
|
- `WasmEd25519Signer`
|
||||||
|
- `WasmChaCha20Poly1305`
|
||||||
|
- `wasm_sha256`, `wasm_sha256_double`, `wasm_hkdf_expand`, and `wasm_derive_encryption_key`
|
||||||
|
|
||||||
|
Raw authenticated login and registration map directly to the Rust WASM layer:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const generated = ed25519_generate();
|
||||||
|
const keyringBytes = keyring_from_ed25519(generated.secretKey, generated.publicKey);
|
||||||
|
|
||||||
|
const registeredId = await rawClient.auth_register(
|
||||||
|
config,
|
||||||
|
hostPublicKeyBytes,
|
||||||
|
keyringBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmedId = await rawClient.auth_connect(
|
||||||
|
config,
|
||||||
|
hostPublicKeyBytes,
|
||||||
|
keyringBytes,
|
||||||
|
registeredId,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
A `WasmClient` manages one active WebTransport session. Create a new instance for independent connections, and call `free()` or `[Symbol.dispose]()` on raw WASM objects when you want to release memory eagerly.
|
||||||
|
|
|
||||||
1
example/Cargo.lock
generated
1
example/Cargo.lock
generated
|
|
@ -952,7 +952,6 @@ dependencies = [
|
||||||
name = "mtp-host"
|
name = "mtp-host"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
"mtp-common",
|
"mtp-common",
|
||||||
"mtp-crypto",
|
"mtp-crypto",
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
println!("Connecting to 127.0.0.1:8080 ...");
|
println!("Connecting to 127.0.0.1:8080 ...");
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig::new("https://127.0.0.1:8080").with_pinned_pem(cert_pem);
|
||||||
url: "https://127.0.0.1:8080".into(),
|
|
||||||
server_cert: Some(cert_pem),
|
|
||||||
client_id: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let server_bundle = host_public_key.clone();
|
let server_bundle = host_public_key.clone();
|
||||||
let (conn, keyring) =
|
let (conn, keyring) =
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tls::export_webtransport_cert_hash(&cert_hash)?;
|
tls::export_webtransport_cert_hash(&cert_hash)?;
|
||||||
println!("WebTransport certificate sha256: {cert_hash}");
|
println!("WebTransport certificate sha256: {cert_hash}");
|
||||||
|
|
||||||
let (host_id, host_keyring) = keys::load_or_generate_host_keys("host_keys.json")?;
|
let (_host_id, host_keyring) = keys::load_or_generate_host_keys("host_keys.json")?;
|
||||||
keys::export_host_public_keys(&host_keyring)?;
|
keys::export_host_public_keys(&host_keyring)?;
|
||||||
|
|
||||||
// The keyring is moved into the host config; keep a copy for decrypting the
|
// The keyring is moved into the host config; keep a copy for decrypting the
|
||||||
|
|
@ -44,7 +44,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let (clients, next_id) = clients::load_client_db("clients.json")?;
|
let (clients, next_id) = clients::load_client_db("clients.json")?;
|
||||||
|
|
||||||
let clients_for_get = clients.clone();
|
let clients_for_get = clients.clone();
|
||||||
let get_existing_user = Box::new(move |id: u64| -> Option<mtp::crypto::PublicKeyBundle> {
|
let get_existing_user = move |id: u64| -> Option<mtp::crypto::PublicKeyBundle> {
|
||||||
let result = clients_for_get.lock().unwrap().get(&id).cloned();
|
let result = clients_for_get.lock().unwrap().get(&id).cloned();
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
println!("Auth lookup: client ID {id} found");
|
println!("Auth lookup: client ID {id} found");
|
||||||
|
|
@ -52,12 +52,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
eprintln!("Auth lookup: unknown client ID {id}");
|
eprintln!("Auth lookup: unknown client ID {id}");
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
});
|
};
|
||||||
|
|
||||||
let clients_for_register = clients.clone();
|
let clients_for_register = clients.clone();
|
||||||
let next_id_for_register = next_id.clone();
|
let next_id_for_register = next_id.clone();
|
||||||
let clients_path = "clients.json".to_string();
|
let clients_path = "clients.json".to_string();
|
||||||
let complete_register = Box::new(move |bundle: mtp::crypto::PublicKeyBundle| -> u64 {
|
let complete_register = move |bundle: mtp::crypto::PublicKeyBundle| -> u64 {
|
||||||
let mut db = clients_for_register.lock().unwrap();
|
let mut db = clients_for_register.lock().unwrap();
|
||||||
let mut nid = next_id_for_register.lock().unwrap();
|
let mut nid = next_id_for_register.lock().unwrap();
|
||||||
let id = *nid;
|
let id = *nid;
|
||||||
|
|
@ -72,26 +72,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
println!("Registered new client with ID: {}", id);
|
println!("Registered new client with ID: {}", id);
|
||||||
id
|
id
|
||||||
});
|
};
|
||||||
|
|
||||||
println!("Starting MTP server on port 8080 ...");
|
println!("Starting MTP server on port 8080 ...");
|
||||||
|
|
||||||
let config = HostConfig {
|
let config = HostConfig::new(
|
||||||
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
|
std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
|
||||||
port: 8080,
|
8080,
|
||||||
tls_fullchain: cert_pem,
|
cert_pem,
|
||||||
tls_key: key_pem,
|
key_pem,
|
||||||
require_authentication: true,
|
)
|
||||||
host_id,
|
.with_authentication(host_keyring, get_existing_user, complete_register);
|
||||||
host_keyring,
|
|
||||||
get_existing_user,
|
|
||||||
complete_register,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut host = MTPHost::new(config).await?;
|
let mut host = MTPHost::new(config).await?;
|
||||||
println!("Server listening on {}", host.local_addr());
|
println!("Server listening on {}", host.local_addr());
|
||||||
|
|
||||||
while let Some(conn) = host.accept().await {
|
while let Some(conn) = host.accept().await? {
|
||||||
println!(
|
println!(
|
||||||
"\n--- New authenticated connection (version {}) ---",
|
"\n--- New authenticated connection (version {}) ---",
|
||||||
conn.version
|
conn.version
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"configVersion": 0,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "mtp-web-client",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.4.0",
|
|
||||||
"vite": "^5.4.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.15", "", { "bin": "bin/nanoid.cjs" }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
|
||||||
|
|
||||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
||||||
|
|
||||||
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,11 +23,11 @@
|
||||||
<label for="host-public-key">Host public key bundle hex</label>
|
<label for="host-public-key">Host public key bundle hex</label>
|
||||||
<textarea id="host-public-key" placeholder="Paste PublicKeyBundle bytes as hex"></textarea>
|
<textarea id="host-public-key" placeholder="Paste PublicKeyBundle bytes as hex"></textarea>
|
||||||
|
|
||||||
<label for="client-public-key">Generated client public key bundle hex</label>
|
<label for="client-credentials">Saved SDK credentials</label>
|
||||||
<textarea id="client-public-key" readonly></textarea>
|
<textarea id="client-credentials" readonly></textarea>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="generate-keypair" type="button">Generate keypair</button>
|
<button id="generate-keypair" type="button">Use new credentials</button>
|
||||||
<button id="connect" type="button" disabled>Connect</button>
|
<button id="connect" type="button" disabled>Connect</button>
|
||||||
<button id="clear-keys" type="button">Clear saved keys</button>
|
<button id="clear-keys" type="button">Clear saved keys</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@11.8.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mtp": "workspace:*"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.4.0",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^5.4.0"
|
"vite": "^8.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,34 @@
|
||||||
import init, {
|
import { MTPClient } from "mtp";
|
||||||
WasmClient,
|
import type { MTPCredentialStorage, MTPLogEvent, ParsedFrame } from "mtp";
|
||||||
ConnectionConfig,
|
|
||||||
ConnectionState,
|
|
||||||
WasmKeyring,
|
|
||||||
ed25519_generate,
|
|
||||||
keyring_from_ed25519,
|
|
||||||
build_demo_message,
|
|
||||||
format_frame,
|
|
||||||
} from "mtp-wasm";
|
|
||||||
|
|
||||||
const STATUS = document.getElementById("status")!;
|
const STATUS = document.getElementById("status")!;
|
||||||
const KEY_STATUS = document.getElementById("key-status")!;
|
const KEY_STATUS = document.getElementById("key-status")!;
|
||||||
const SERVER_URL = document.getElementById("server-url") as HTMLInputElement;
|
const SERVER_URL = document.getElementById("server-url") as HTMLInputElement;
|
||||||
const HOST_PUBLIC_KEY = document.getElementById("host-public-key") as HTMLTextAreaElement;
|
const HOST_PUBLIC_KEY = document.getElementById("host-public-key") as HTMLTextAreaElement;
|
||||||
const CLIENT_PUBLIC_KEY = document.getElementById("client-public-key") as HTMLTextAreaElement;
|
const CLIENT_CREDENTIALS = document.getElementById("client-credentials") as HTMLTextAreaElement;
|
||||||
const GENERATE_KEYPAIR = document.getElementById("generate-keypair") as HTMLButtonElement;
|
const GENERATE_KEYPAIR = document.getElementById("generate-keypair") as HTMLButtonElement;
|
||||||
const CONNECT = document.getElementById("connect") as HTMLButtonElement;
|
const CONNECT = document.getElementById("connect") as HTMLButtonElement;
|
||||||
const CLEAR_KEYS = document.getElementById("clear-keys") as HTMLButtonElement;
|
const CLEAR_KEYS = document.getElementById("clear-keys") as HTMLButtonElement;
|
||||||
|
|
||||||
const STORAGE_KEY = "mtp-web-client-keys";
|
const CREDENTIALS_KEY = "mtp-web-client-credentials";
|
||||||
|
const HOST_PUBLIC_KEY_KEY = "mtp-web-client-host-public-key";
|
||||||
|
|
||||||
type SavedKeys = {
|
type SavedKeys = {
|
||||||
clientId: string | null;
|
clientId: string | null;
|
||||||
keyring: number[];
|
keyring?: number[];
|
||||||
|
keyringBytes?: number[];
|
||||||
hostPublicKey?: number[];
|
hostPublicKey?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyringBytes: Uint8Array | null = null;
|
|
||||||
let clientId: bigint | null = null;
|
let clientId: bigint | null = null;
|
||||||
let devCertHash = "";
|
let devCertHash = "";
|
||||||
|
|
||||||
|
const credentialStorage: MTPCredentialStorage = {
|
||||||
|
getItem: (key) => localStorage.getItem(key),
|
||||||
|
setItem: (key, value) => localStorage.setItem(key, value),
|
||||||
|
removeItem: (key) => localStorage.removeItem(key),
|
||||||
|
};
|
||||||
|
|
||||||
function log(msg: string, cls = "") {
|
function log(msg: string, cls = "") {
|
||||||
const line = document.createElement("div");
|
const line = document.createElement("div");
|
||||||
line.textContent = msg;
|
line.textContent = msg;
|
||||||
|
|
@ -37,6 +36,35 @@ function log(msg: string, cls = "") {
|
||||||
STATUS.appendChild(line);
|
STATUS.appendChild(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStructured(value: unknown): string {
|
||||||
|
return JSON.stringify(value, (_key, item) => {
|
||||||
|
if (typeof item === "bigint") {
|
||||||
|
return item.toString();
|
||||||
|
}
|
||||||
|
if (item instanceof Uint8Array) {
|
||||||
|
return { bytes: item.length, hex: bytesToHex(item.slice(0, 32)) };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatParsedFrame(frame: ParsedFrame): string {
|
||||||
|
return renderStructured({
|
||||||
|
id: frame.id,
|
||||||
|
type: frame.type,
|
||||||
|
sender: frame.sender,
|
||||||
|
receiver: frame.receiver,
|
||||||
|
data: frame.data,
|
||||||
|
rawBytes: frame.raw.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoggerEvent(event: MTPLogEvent): string {
|
||||||
|
return event.hint === "error"
|
||||||
|
? `[${event.hint}] ${event.type}: ${event.error}`
|
||||||
|
: `[${event.hint}] ${event.type}: ${renderStructured(event.data)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function setKeyStatus(msg: string) {
|
function setKeyStatus(msg: string) {
|
||||||
KEY_STATUS.textContent = msg;
|
KEY_STATUS.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
@ -57,37 +85,35 @@ function hexToBytes(value: string): Uint8Array {
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveKeys() {
|
function saveHostPublicKey() {
|
||||||
if (!keyringBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hostPublicKey: number[] | undefined;
|
|
||||||
try {
|
try {
|
||||||
hostPublicKey = Array.from(hexToBytes(HOST_PUBLIC_KEY.value));
|
localStorage.setItem(HOST_PUBLIC_KEY_KEY, bytesToHex(hexToBytes(HOST_PUBLIC_KEY.value)));
|
||||||
} catch {
|
} catch {
|
||||||
hostPublicKey = undefined;
|
localStorage.removeItem(HOST_PUBLIC_KEY_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: SavedKeys = {
|
|
||||||
clientId: clientId?.toString() ?? null,
|
|
||||||
keyring: Array.from(keyringBytes),
|
|
||||||
hostPublicKey,
|
|
||||||
};
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadKeys() {
|
function loadKeys() {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(CREDENTIALS_KEY);
|
||||||
|
const savedHostPublicKey = localStorage.getItem(HOST_PUBLIC_KEY_KEY);
|
||||||
|
if (savedHostPublicKey) {
|
||||||
|
HOST_PUBLIC_KEY.value = savedHostPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
setKeyStatus("No client keypair generated yet.");
|
CLIENT_CREDENTIALS.value = "";
|
||||||
|
setKeyStatus("No saved SDK credentials. The next connection will generate and store a reusable keyring.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(raw) as SavedKeys;
|
const data = JSON.parse(raw) as SavedKeys;
|
||||||
keyringBytes = new Uint8Array(data.keyring);
|
|
||||||
clientId = data.clientId ? BigInt(data.clientId) : null;
|
clientId = data.clientId ? BigInt(data.clientId) : null;
|
||||||
CLIENT_PUBLIC_KEY.value = publicKeyHexFromKeyring(keyringBytes);
|
const keyringLength = (data.keyring ?? data.keyringBytes ?? []).length;
|
||||||
|
CLIENT_CREDENTIALS.value = renderStructured({
|
||||||
|
clientId: data.clientId,
|
||||||
|
keyringBytes: keyringLength,
|
||||||
|
hostPublicKeyBytes: data.hostPublicKey?.length ?? 0,
|
||||||
|
});
|
||||||
if (data.hostPublicKey) {
|
if (data.hostPublicKey) {
|
||||||
HOST_PUBLIC_KEY.value = bytesToHex(new Uint8Array(data.hostPublicKey));
|
HOST_PUBLIC_KEY.value = bytesToHex(new Uint8Array(data.hostPublicKey));
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +134,7 @@ async function loadHostPublicKey() {
|
||||||
if (!hostPublicKey) return;
|
if (!hostPublicKey) return;
|
||||||
|
|
||||||
HOST_PUBLIC_KEY.value = hostPublicKey;
|
HOST_PUBLIC_KEY.value = hostPublicKey;
|
||||||
saveKeys();
|
saveHostPublicKey();
|
||||||
log(`Loaded host public key bundle (${hostPublicKey.length / 2} bytes).`);
|
log(`Loaded host public key bundle (${hostPublicKey.length / 2} bytes).`);
|
||||||
} catch {
|
} catch {
|
||||||
// Manual paste still works when the server has not exported the file yet.
|
// Manual paste still works when the server has not exported the file yet.
|
||||||
|
|
@ -131,109 +157,82 @@ async function loadDevCertHash() {
|
||||||
|
|
||||||
async function initWasm() {
|
async function initWasm() {
|
||||||
log("Loading WASM module...");
|
log("Loading WASM module...");
|
||||||
await init();
|
await MTPClient.create({ url: SERVER_URL.value, storage: credentialStorage, credentialsStorageKey: CREDENTIALS_KEY });
|
||||||
log(`WASM loaded. WebTransport supported: ${WasmClient.is_supported()}`);
|
const supported = MTPClient.isSupported();
|
||||||
CONNECT.disabled = !WasmClient.is_supported();
|
log(`WASM loaded. WebTransport supported: ${supported}`);
|
||||||
}
|
CONNECT.disabled = !supported;
|
||||||
|
|
||||||
function createClient(): WasmClient {
|
|
||||||
return new WasmClient(
|
|
||||||
(state: number) =>
|
|
||||||
log(`[state] ${ConnectionState[state] ?? state}`, "state"),
|
|
||||||
(data: Uint8Array) => {
|
|
||||||
try {
|
|
||||||
log(`Received: ${format_frame(data)}`, "received");
|
|
||||||
} catch (e) {
|
|
||||||
log(`[message parse error] ${e}`, "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err: any) => log(`[error] ${err}`, "error"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateKeyringBytes(): Uint8Array {
|
|
||||||
const gen = ed25519_generate();
|
|
||||||
const sk = gen.secretKey as Uint8Array;
|
|
||||||
const pk = gen.publicKey as Uint8Array;
|
|
||||||
gen.signer.free();
|
|
||||||
return keyring_from_ed25519(sk, pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
function publicKeyHexFromKeyring(bytes: Uint8Array): string {
|
|
||||||
const keyring = WasmKeyring.from_bytes(bytes);
|
|
||||||
const publicBundle = keyring.public_key_bundle();
|
|
||||||
const publicHex = bytesToHex(publicBundle.to_bytes());
|
|
||||||
publicBundle.free();
|
|
||||||
keyring.free();
|
|
||||||
return publicHex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
STATUS.textContent = "";
|
STATUS.textContent = "";
|
||||||
|
|
||||||
if (!WasmClient.is_supported()) {
|
if (!MTPClient.isSupported()) {
|
||||||
log("WebTransport is not supported in this browser.", "error");
|
log("WebTransport is not supported in this browser.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!keyringBytes) {
|
|
||||||
log("Generate a client keypair first.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostPk = hexToBytes(HOST_PUBLIC_KEY.value);
|
const hostPk = hexToBytes(HOST_PUBLIC_KEY.value);
|
||||||
|
saveHostPublicKey();
|
||||||
await loadDevCertHash();
|
await loadDevCertHash();
|
||||||
|
|
||||||
const client = createClient();
|
|
||||||
const serverUrl = SERVER_URL.value.trim();
|
const serverUrl = SERVER_URL.value.trim();
|
||||||
const config = new ConnectionConfig(serverUrl);
|
const serverCertificateHashes = devCertHash ? [`sha-256:${devCertHash}`] : undefined;
|
||||||
if (devCertHash) {
|
if (serverCertificateHashes) {
|
||||||
log(`Pinning WebTransport certificate hash: sha-256:${devCertHash}`);
|
log(`Pinning WebTransport certificate hash: ${serverCertificateHashes[0]}`);
|
||||||
config.server_certificate_hashes = [`sha-256:${devCertHash}`];
|
|
||||||
} else {
|
} else {
|
||||||
log("No WebTransport certificate hash loaded; relying on browser trust store.", "state");
|
log("No WebTransport certificate hash loaded; relying on browser trust store.", "state");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let activeClientId: bigint;
|
const client = await MTPClient.create({
|
||||||
if (clientId !== null) {
|
url: serverUrl,
|
||||||
log(`Using saved client ID ${clientId}...`);
|
hostPublicKey: hostPk,
|
||||||
activeClientId = await client.auth_connect(
|
storage: credentialStorage,
|
||||||
config,
|
credentialsStorageKey: CREDENTIALS_KEY,
|
||||||
hostPk,
|
serverCertificateHashes,
|
||||||
keyringBytes,
|
pings: { intervalMs: 30_000 },
|
||||||
clientId,
|
logger(event) {
|
||||||
);
|
log(renderLoggerEvent(event), event.hint === "error" ? "error" : event.type === "state" ? "state" : "");
|
||||||
log(`Authenticated as client ${activeClientId}`);
|
},
|
||||||
} else {
|
});
|
||||||
log("Registering generated client keypair...");
|
|
||||||
activeClientId = await client.auth_register(config, hostPk, keyringBytes);
|
|
||||||
clientId = activeClientId;
|
|
||||||
saveKeys();
|
|
||||||
log(`Registered with ID: ${activeClientId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log("\nSending demo message...");
|
client.subscribe("Pong", (frame: ParsedFrame) => {
|
||||||
const frame = build_demo_message(activeClientId, keyringBytes, hostPk);
|
log(`Subscribed Pong: ${formatParsedFrame(frame)}`, "received");
|
||||||
log(`Sending: ${format_frame(frame)}`, "state");
|
});
|
||||||
await client.send(frame);
|
|
||||||
log(`Sent ${frame.length} bytes`);
|
const activeClientId = await client.connectOrRegister();
|
||||||
|
clientId = activeClientId;
|
||||||
|
loadKeys();
|
||||||
|
log(`Connected as client ${activeClientId}`);
|
||||||
|
|
||||||
|
log("\nSending typed Ping...");
|
||||||
|
await client.send("Ping", {
|
||||||
|
Description: "MTP web client send ping",
|
||||||
|
Timestamp: BigInt(Date.now()),
|
||||||
|
}, { sender: activeClientId });
|
||||||
|
log("Typed Ping sent.");
|
||||||
|
|
||||||
|
log("\nRequesting Pong by Ping frame id...");
|
||||||
|
const response = await client.request("Ping", {
|
||||||
|
Description: "MTP web client request ping",
|
||||||
|
Timestamp: BigInt(Date.now()),
|
||||||
|
}, { sender: activeClientId, responseType: "Pong" });
|
||||||
|
log(`Request response: ${formatParsedFrame(response)}`, "received");
|
||||||
|
|
||||||
log("\nClient running. Waiting for incoming messages...");
|
log("\nClient running. Waiting for incoming messages...");
|
||||||
} finally {
|
} catch (error) {
|
||||||
config.free();
|
log(`[error] ${error}`, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GENERATE_KEYPAIR.addEventListener("click", () => {
|
GENERATE_KEYPAIR.addEventListener("click", () => {
|
||||||
try {
|
try {
|
||||||
keyringBytes = generateKeyringBytes();
|
|
||||||
clientId = null;
|
clientId = null;
|
||||||
saveKeys();
|
localStorage.removeItem(CREDENTIALS_KEY);
|
||||||
CLIENT_PUBLIC_KEY.value = publicKeyHexFromKeyring(keyringBytes);
|
CLIENT_CREDENTIALS.value = "";
|
||||||
setKeyStatus("Generated client keypair. Not registered yet.");
|
setKeyStatus("Cleared saved credentials. The next connection will generate a new reusable keyring.");
|
||||||
log("Generated and saved a new client keypair.");
|
log("Cleared saved SDK credentials.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`Key generation failed: ${e}`, "error");
|
log(`Credential reset failed: ${e}`, "error");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -242,7 +241,7 @@ CONNECT.addEventListener("click", () => {
|
||||||
connect().catch((e) => {
|
connect().catch((e) => {
|
||||||
log(`Fatal error: ${e}`, "error");
|
log(`Fatal error: ${e}`, "error");
|
||||||
log(
|
log(
|
||||||
`[fatal context] clientId=${clientId?.toString() ?? "unregistered"}, server=${SERVER_URL.value.trim()}, hostPkChars=${HOST_PUBLIC_KEY.value.replace(/[^0-9a-fA-F]/g, "").length}, keyringBytes=${keyringBytes?.length ?? 0}, certHash=${devCertHash || "none"}`,
|
`[fatal context] clientId=${clientId?.toString() ?? "unregistered"}, server=${SERVER_URL.value.trim()}, hostPkChars=${HOST_PUBLIC_KEY.value.replace(/[^0-9a-fA-F]/g, "").length}, hasStoredCredentials=${localStorage.getItem(CREDENTIALS_KEY) ? "yes" : "no"}, certHash=${devCertHash || "none"}`,
|
||||||
"error",
|
"error",
|
||||||
);
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -250,15 +249,15 @@ CONNECT.addEventListener("click", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
CLEAR_KEYS.addEventListener("click", () => {
|
CLEAR_KEYS.addEventListener("click", () => {
|
||||||
keyringBytes = null;
|
|
||||||
clientId = null;
|
clientId = null;
|
||||||
CLIENT_PUBLIC_KEY.value = "";
|
CLIENT_CREDENTIALS.value = "";
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(CREDENTIALS_KEY);
|
||||||
setKeyStatus("No client keypair generated yet.");
|
localStorage.removeItem(HOST_PUBLIC_KEY_KEY);
|
||||||
log("Cleared saved client keys.");
|
setKeyStatus("No saved SDK credentials.");
|
||||||
|
log("Cleared saved SDK credentials and host public key.");
|
||||||
});
|
});
|
||||||
|
|
||||||
HOST_PUBLIC_KEY.addEventListener("change", saveKeys);
|
HOST_PUBLIC_KEY.addEventListener("change", saveHostPublicKey);
|
||||||
|
|
||||||
initWasm()
|
initWasm()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { build_demo_message, build_ping_frame, parse_auth_response } from 'mtp-wasm';
|
|
||||||
|
|
||||||
export function buildAuthResponse(
|
|
||||||
response: Uint8Array,
|
|
||||||
): {
|
|
||||||
connected: boolean;
|
|
||||||
clientNonce: Uint8Array;
|
|
||||||
assignedId: bigint;
|
|
||||||
timestamp: bigint;
|
|
||||||
signature: Uint8Array;
|
|
||||||
} {
|
|
||||||
return parse_auth_response(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildDemoMessage(
|
|
||||||
clientId: bigint,
|
|
||||||
keyringBytes: Uint8Array,
|
|
||||||
hostBundle: Uint8Array,
|
|
||||||
): Uint8Array {
|
|
||||||
return build_demo_message(clientId, keyringBytes, hostBundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildPingFrame(
|
|
||||||
clientId: bigint,
|
|
||||||
description: string,
|
|
||||||
timestamp: bigint,
|
|
||||||
data?: Uint8Array,
|
|
||||||
): Uint8Array {
|
|
||||||
return build_ping_frame(clientId, description, timestamp, data ?? new Uint8Array());
|
|
||||||
}
|
|
||||||
|
|
@ -8,11 +8,7 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true
|
||||||
"paths": {
|
|
||||||
"mtp-wasm": ["../../wasm/pkg"]
|
|
||||||
},
|
|
||||||
"rootDir": "../.."
|
|
||||||
},
|
},
|
||||||
"include": ["src", "../../wasm/pkg"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { mtp } from 'mtp/vite';
|
||||||
|
|
||||||
const devCertDir = path.resolve(__dirname, '../dev-cert');
|
const devCertDir = path.resolve(__dirname, '../dev-cert');
|
||||||
const certPath = process.env.MTP_DEV_CERT ?? path.join(devCertDir, 'cert.pem');
|
const certPath = process.env.MTP_DEV_CERT ?? path.join(devCertDir, 'cert.pem');
|
||||||
|
|
@ -9,11 +10,7 @@ const keyPath = process.env.MTP_DEV_KEY ?? path.join(devCertDir, 'key.pem');
|
||||||
const hasDevCert = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
const hasDevCert = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
plugins: [mtp({ typeMaps: '../../example/type-maps.yaml' })],
|
||||||
alias: {
|
|
||||||
'mtp-wasm': path.resolve(__dirname, '../../wasm/pkg'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
https: hasDevCert
|
https: hasDevCert
|
||||||
? {
|
? {
|
||||||
|
|
@ -21,8 +18,5 @@ export default defineConfig({
|
||||||
key: fs.readFileSync(keyPath),
|
key: fs.readFileSync(keyPath),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
fs: {
|
|
||||||
allow: ['.', path.resolve(__dirname, '../../wasm/pkg')],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
27
flake.nix
27
flake.nix
|
|
@ -32,17 +32,7 @@
|
||||||
runtimeInputs = [rustToolchain];
|
runtimeInputs = [rustToolchain];
|
||||||
text = ''
|
text = ''
|
||||||
export MTP_TYPE_MAPS="''${MTP_TYPE_MAPS:-$PWD/example/type-maps.yaml}"
|
export MTP_TYPE_MAPS="''${MTP_TYPE_MAPS:-$PWD/example/type-maps.yaml}"
|
||||||
cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings \
|
cargo clippy --workspace --exclude mtp-wasm --all-targets --all-features -- -D warnings -W unreachable-pub
|
||||||
-W unreachable-pub \
|
|
||||||
-W clippy::cognitive_complexity \
|
|
||||||
-W clippy::missing_docs_in_private_items \
|
|
||||||
-W clippy::missing_errors_doc \
|
|
||||||
-W clippy::missing_panics_doc \
|
|
||||||
-W clippy::missing_safety_doc \
|
|
||||||
-W clippy::undocumented_unsafe_blocks \
|
|
||||||
-W clippy::pedantic \
|
|
||||||
-W clippy::restriction \
|
|
||||||
-A clippy::blanket_clippy_restriction_lints
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,21 +46,20 @@
|
||||||
|
|
||||||
buildAll = pkgs.writeShellApplication {
|
buildAll = pkgs.writeShellApplication {
|
||||||
name = "mtp-build-all";
|
name = "mtp-build-all";
|
||||||
runtimeInputs = [rustToolchain pkgs.wasm-pack pkgs.bun clippyCheck macheteCheck];
|
runtimeInputs = [rustToolchain pkgs.wasm-pack pkgs.pnpm pkgs.coreutils clippyCheck macheteCheck];
|
||||||
text = ''
|
text = ''
|
||||||
export MTP_TYPE_MAPS="''${MTP_TYPE_MAPS:-$PWD/example/type-maps.yaml}"
|
export MTP_TYPE_MAPS="''${MTP_TYPE_MAPS:-$PWD/example/type-maps.yaml}"
|
||||||
|
|
||||||
bun install --frozen-lockfile
|
timeout 60s pnpm install --frozen-lockfile
|
||||||
cargo fmt --all --check
|
cargo fmt --all --check
|
||||||
cargo b
|
cargo b
|
||||||
cargo test --workspace --exclude mtp-wasm --all-features
|
cargo test --workspace --exclude mtp-wasm --all-features
|
||||||
cargo check --manifest-path example/Cargo.toml --workspace --all-targets --all-features
|
cargo check --manifest-path example/Cargo.toml --workspace --all-targets --all-features
|
||||||
mtp-clippy
|
mtp-clippy
|
||||||
mtp-machete
|
mtp-machete
|
||||||
bun run dup
|
pnpm run dup
|
||||||
RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build wasm --target web --out-dir pkg --release
|
pnpm run build
|
||||||
bun install --cwd example/web-client --frozen-lockfile
|
pnpm --filter mtp-web-client run build
|
||||||
bun run --cwd example/web-client build
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -91,6 +80,7 @@
|
||||||
rustToolchain
|
rustToolchain
|
||||||
cargo-machete
|
cargo-machete
|
||||||
wasm-pack
|
wasm-pack
|
||||||
|
pnpm
|
||||||
pkg-config
|
pkg-config
|
||||||
openssl
|
openssl
|
||||||
];
|
];
|
||||||
|
|
@ -140,9 +130,10 @@
|
||||||
name = "autoStart";
|
name = "autoStart";
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
mprocs
|
mprocs
|
||||||
|
pnpm
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
nix develop --command bash -c "mprocs 'cd wasm && wasm-pack build --target web --out-dir pkg && cd ../example/web-client && bun dev' 'cargo b && cd example && cargo r --bin server' 'cd example && cargo r --bin client'"
|
nix develop --command bash -c "mprocs 'cd wasm && wasm-pack build --target web --out-dir pkg && cd ../example/web-client && pnpm -r update mtp --latest && pnpm dev' 'cargo b && cd example && cargo r --bin server' 'cd example && cargo r --bin client'"
|
||||||
exit
|
exit
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ mtp-codec = { path = "../codec", features = ["registry"] }
|
||||||
mtp-transport = { path = "../transport", features = ["host"] }
|
mtp-transport = { path = "../transport", features = ["host"] }
|
||||||
mtp-crypto = { path = "../crypto", optional = true }
|
mtp-crypto = { path = "../crypto", optional = true }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
log = "0.4"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
crypto = ["dep:mtp-crypto", "mtp-codec/crypto"]
|
crypto = ["dep:mtp-crypto", "mtp-codec/crypto"]
|
||||||
|
|
|
||||||
188
host/src/lib.rs
188
host/src/lib.rs
|
|
@ -5,6 +5,7 @@ use mtp_codec::{
|
||||||
use mtp_common::CommunicationError;
|
use mtp_common::CommunicationError;
|
||||||
use mtp_transport::{Policy, Receiver, Sender};
|
use mtp_transport::{Policy, Receiver, Sender};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
/* Host configuration. */
|
/* Host configuration. */
|
||||||
pub struct HostConfig {
|
pub struct HostConfig {
|
||||||
|
|
@ -16,15 +17,81 @@ pub struct HostConfig {
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub require_authentication: bool,
|
pub require_authentication: bool,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub host_id: u64,
|
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
pub host_keyring: mtp_crypto::Keyring,
|
pub host_keyring: mtp_crypto::Keyring,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub get_existing_user: Box<dyn Fn(u64) -> Option<mtp_crypto::PublicKeyBundle> + Send>,
|
pub get_existing_user: Box<dyn Fn(u64) -> Option<mtp_crypto::PublicKeyBundle> + Send + Sync>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub complete_register: Box<dyn Fn(mtp_crypto::PublicKeyBundle) -> u64 + Send>,
|
pub complete_register: Box<dyn Fn(mtp_crypto::PublicKeyBundle) -> u64 + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HostConfig {
|
||||||
|
pub fn new(ip: IpAddr, port: u16, tls_fullchain: Vec<u8>, tls_key: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
tls_fullchain,
|
||||||
|
tls_key,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
require_authentication: false,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
host_keyring: mtp_crypto::Keyring::new(
|
||||||
|
mtp_crypto::KemPublicKey::new(Vec::new()),
|
||||||
|
mtp_crypto::KemPrivateKey::new(Vec::new()),
|
||||||
|
mtp_crypto::SignaturePqPublicKey::new(Vec::new()),
|
||||||
|
mtp_crypto::SignaturePqPrivateKey::new(Vec::new()),
|
||||||
|
mtp_crypto::SignaturePublicKey::new(Vec::new()),
|
||||||
|
mtp_crypto::SignaturePrivateKey::new(Vec::new()),
|
||||||
|
),
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
get_existing_user: Box::new(|_| None),
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
complete_register: Box::new(|_| 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub fn with_authentication(
|
||||||
|
mut self,
|
||||||
|
host_keyring: mtp_crypto::Keyring,
|
||||||
|
get_existing_user: impl Fn(u64) -> Option<mtp_crypto::PublicKeyBundle> + Send + Sync + 'static,
|
||||||
|
complete_register: impl Fn(mtp_crypto::PublicKeyBundle) -> u64 + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.require_authentication = true;
|
||||||
|
self.host_keyring = host_keyring;
|
||||||
|
self.get_existing_user = Box::new(get_existing_user);
|
||||||
|
self.complete_register = Box::new(complete_register);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AcceptError {
|
||||||
|
Receive(CommunicationError),
|
||||||
|
MissingVersion,
|
||||||
|
UnsupportedVersion(Version),
|
||||||
|
AuthenticationFailed(String),
|
||||||
|
Send(CommunicationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AcceptError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Receive(error) => write!(f, "failed to receive opening message: {error}"),
|
||||||
|
Self::MissingVersion => write!(
|
||||||
|
f,
|
||||||
|
"opening message did not include a valid protocol version"
|
||||||
|
),
|
||||||
|
Self::UnsupportedVersion(version) => {
|
||||||
|
write!(f, "unsupported protocol version: {version}")
|
||||||
|
}
|
||||||
|
Self::AuthenticationFailed(reason) => write!(f, "authentication failed: {reason}"),
|
||||||
|
Self::Send(error) => write!(f, "failed to send handshake message: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for AcceptError {}
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AuthState {
|
pub enum AuthState {
|
||||||
|
|
@ -80,16 +147,13 @@ impl MTPHost {
|
||||||
* Accept an incoming connection, negotiate the protocol version,
|
* Accept an incoming connection, negotiate the protocol version,
|
||||||
* and return a ready-to-use `MTPConnection`.
|
* and return a ready-to-use `MTPConnection`.
|
||||||
*
|
*
|
||||||
* Returns `None` if the connection is closed or the version is
|
* Returns `Ok(None)` if the listener is closed. Handshake and version
|
||||||
* incompatible.
|
* negotiation failures are returned explicitly.
|
||||||
*/
|
*/
|
||||||
pub async fn accept(&mut self) -> Option<MTPConnection> {
|
pub async fn accept(&mut self) -> Result<Option<MTPConnection>, AcceptError> {
|
||||||
let (sender, receiver) = match self.transport.next().await {
|
let (sender, receiver) = match self.transport.next().await {
|
||||||
Some(pair) => pair,
|
Some(pair) => pair,
|
||||||
None => {
|
None => return Ok(None),
|
||||||
log::warn!("accept: transport.next() returned None (listener closed)");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -100,10 +164,7 @@ impl MTPHost {
|
||||||
// Read the first message (always encoded with reserved types).
|
// Read the first message (always encoded with reserved types).
|
||||||
let first_msg = match receiver.receive().await {
|
let first_msg = match receiver.receive().await {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => return Err(AcceptError::Receive(e)),
|
||||||
log::warn!("accept: receive failed: {e:?}");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -115,13 +176,7 @@ impl MTPHost {
|
||||||
*/
|
*/
|
||||||
let client_version = match extract_version(&first_msg) {
|
let client_version = match extract_version(&first_msg) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => return Err(AcceptError::MissingVersion),
|
||||||
log::warn!(
|
|
||||||
"accept: extract_version failed on msg type {:?}",
|
|
||||||
first_msg.get_type()
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let negotiated = match self
|
let negotiated = match self
|
||||||
|
|
@ -129,15 +184,12 @@ impl MTPHost {
|
||||||
.negotiate(std::slice::from_ref(&client_version))
|
.negotiate(std::slice::from_ref(&client_version))
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||||
log::warn!("accept: negotiate failed for client version {client_version:?}");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let codec = VersionedCodec::new(self.registry.clone());
|
let codec = VersionedCodec::new(self.registry.clone());
|
||||||
|
|
||||||
Some(MTPConnection {
|
Ok(Some(MTPConnection {
|
||||||
version: negotiated,
|
version: negotiated,
|
||||||
codec,
|
codec,
|
||||||
sender,
|
sender,
|
||||||
|
|
@ -148,7 +200,7 @@ impl MTPHost {
|
||||||
client_id: 0,
|
client_id: 0,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
client_public_key: None,
|
client_public_key: None,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_addr(&self) -> std::net::SocketAddr {
|
pub fn local_addr(&self) -> std::net::SocketAddr {
|
||||||
|
|
@ -180,7 +232,7 @@ impl MTPHost {
|
||||||
&mut self,
|
&mut self,
|
||||||
sender: Sender,
|
sender: Sender,
|
||||||
receiver: Receiver,
|
receiver: Receiver,
|
||||||
) -> Option<MTPConnection> {
|
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||||
use mtp_crypto::{
|
use mtp_crypto::{
|
||||||
Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519,
|
Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519,
|
||||||
verify_ml_dsa,
|
verify_ml_dsa,
|
||||||
|
|
@ -207,32 +259,36 @@ impl MTPHost {
|
||||||
.is_empty();
|
.is_empty();
|
||||||
|
|
||||||
// Sign `payload` with the host keys (Ed25519 always, ML-DSA when configured).
|
// Sign `payload` with the host keys (Ed25519 always, ML-DSA when configured).
|
||||||
let host_sign = |payload: &[u8]| -> Option<(Vec<u8>, Vec<u8>)> {
|
let host_sign = |payload: &[u8]| -> Result<(Vec<u8>, Vec<u8>), AcceptError> {
|
||||||
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key).ok()?;
|
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key)
|
||||||
let sig = signer.sign(payload).ok()?;
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
|
let sig = signer
|
||||||
|
.sign(payload)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
let pq_sig = if pq_enabled {
|
let pq_sig = if pq_enabled {
|
||||||
let pq = MlDsaSigner::new(
|
let pq = MlDsaSigner::new(
|
||||||
&self.config.host_keyring.sig_pq_secret_key,
|
&self.config.host_keyring.sig_pq_secret_key,
|
||||||
&self.config.host_keyring.sig_pq_public_key,
|
&self.config.host_keyring.sig_pq_public_key,
|
||||||
)
|
)
|
||||||
.ok()?;
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
pq.sign(payload).ok()?
|
pq.sign(payload)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
Some((sig, pq_sig))
|
Ok((sig, pq_sig))
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Step 1: receive the client's unsigned hello =====
|
// ===== Step 1: receive the client's unsigned hello =====
|
||||||
let hello = receiver.receive().await.ok()?;
|
let hello = receiver.receive().await.map_err(AcceptError::Receive)?;
|
||||||
let version_str = match hello.get_data(DataType::Version.to_id(&tm)) {
|
let version_str = match hello.get_data(DataType::Version.to_id(&tm)) {
|
||||||
DataValue::Str(s) => s.clone(),
|
DataValue::Str(s) => s.clone(),
|
||||||
_ => {
|
_ => {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::MissingVersion);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let client_version = Version::parse(&version_str)?;
|
let client_version = Version::parse(&version_str).ok_or(AcceptError::MissingVersion)?;
|
||||||
|
|
||||||
let (flow, response_type) =
|
let (flow, response_type) =
|
||||||
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
||||||
|
|
@ -241,7 +297,9 @@ impl MTPHost {
|
||||||
DataValue::UnsignedNumber(n) => *n as u64,
|
DataValue::UnsignedNumber(n) => *n as u64,
|
||||||
_ => {
|
_ => {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"missing client id".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let bundle = match (self.config.get_existing_user)(cid) {
|
let bundle = match (self.config.get_existing_user)(cid) {
|
||||||
|
|
@ -253,7 +311,9 @@ impl MTPHost {
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
|
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
|
||||||
let _ = sender.send(&rejection).await;
|
let _ = sender.send(&rejection).await;
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"unknown client id".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
|
|
@ -263,10 +323,14 @@ impl MTPHost {
|
||||||
} else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
|
} else if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
|
||||||
// REGISTER: the client presents the bundle it wants to register.
|
// REGISTER: the client presents the bundle it wants to register.
|
||||||
let bundle = match hello.get_data(DataType::PublicKeys.to_id(&tm)) {
|
let bundle = match hello.get_data(DataType::PublicKeys.to_id(&tm)) {
|
||||||
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).ok()?,
|
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| {
|
||||||
|
AcceptError::AuthenticationFailed("invalid public key bundle".into())
|
||||||
|
})?,
|
||||||
_ => {
|
_ => {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"missing public keys".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let pk_bytes = bundle.as_bytes();
|
let pk_bytes = bundle.as_bytes();
|
||||||
|
|
@ -276,7 +340,9 @@ impl MTPHost {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"unexpected authentication message".into(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// The id bound into the challenge (0 for register: none assigned yet).
|
// The id bound into the challenge (0 for register: none assigned yet).
|
||||||
|
|
@ -300,26 +366,35 @@ impl MTPHost {
|
||||||
challenge_msg = challenge_msg
|
challenge_msg = challenge_msg
|
||||||
.add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig));
|
.add_typed_default(DataType::PqSignature, DataValue::Bytes(chal_pq_sig));
|
||||||
}
|
}
|
||||||
sender.send(&challenge_msg).await.ok()?;
|
sender
|
||||||
|
.send(&challenge_msg)
|
||||||
|
.await
|
||||||
|
.map_err(AcceptError::Send)?;
|
||||||
|
|
||||||
// ===== Step 3: receive and verify the client's proof =====
|
// ===== Step 3: receive and verify the client's proof =====
|
||||||
let proof = receiver.receive().await.ok()?;
|
let proof = receiver.receive().await.map_err(AcceptError::Receive)?;
|
||||||
if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) {
|
if proof.get_type() != mtp_codec::CommunicationType::ChallengeResponse.to_id(&tm) {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"missing challenge response".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let client_nonce = match proof.get_data(DataType::ClientNonce.to_id(&tm)) {
|
let client_nonce = match proof.get_data(DataType::ClientNonce.to_id(&tm)) {
|
||||||
DataValue::UnsignedNumber(n) => *n,
|
DataValue::UnsignedNumber(n) => *n,
|
||||||
_ => {
|
_ => {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"missing client nonce".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) {
|
let sig_bytes = match proof.get_data(DataType::Signature.to_id(&tm)) {
|
||||||
DataValue::Bytes(b) => b.clone(),
|
DataValue::Bytes(b) => b.clone(),
|
||||||
_ => {
|
_ => {
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"missing challenge signature".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature.to_id(&tm)) {
|
let pq_sig_bytes: Vec<u8> = match proof.get_data(DataType::PqSignature.to_id(&tm)) {
|
||||||
|
|
@ -355,7 +430,9 @@ impl MTPHost {
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
|
.add_typed_default(DataType::Connected, DataValue::BoolFalse);
|
||||||
let _ = sender.send(&rejection).await;
|
let _ = sender.send(&rejection).await;
|
||||||
sender.close();
|
sender.close();
|
||||||
return None;
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"client proof signature invalid".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proof verified: resolve the assigned id and retain the client's bundle.
|
// Proof verified: resolve the assigned id and retain the client's bundle.
|
||||||
|
|
@ -387,14 +464,17 @@ impl MTPHost {
|
||||||
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
|
response.add_typed_default(DataType::PqSignature, DataValue::Bytes(host_pq_sig));
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.send(&response).await.ok()?;
|
sender.send(&response).await.map_err(AcceptError::Send)?;
|
||||||
sender.finish_stream().await.ok()?;
|
sender.finish_stream().await.map_err(AcceptError::Send)?;
|
||||||
|
|
||||||
// ===== Version negotiation =====
|
// ===== Version negotiation =====
|
||||||
let negotiated = self.registry.negotiate(&[client_version])?;
|
let negotiated = self
|
||||||
|
.registry
|
||||||
|
.negotiate(std::slice::from_ref(&client_version))
|
||||||
|
.ok_or(AcceptError::UnsupportedVersion(client_version))?;
|
||||||
let codec = VersionedCodec::new(self.registry.clone());
|
let codec = VersionedCodec::new(self.registry.clone());
|
||||||
|
|
||||||
Some(MTPConnection {
|
Ok(Some(MTPConnection {
|
||||||
version: negotiated,
|
version: negotiated,
|
||||||
codec,
|
codec,
|
||||||
sender,
|
sender,
|
||||||
|
|
@ -402,7 +482,7 @@ impl MTPHost {
|
||||||
auth_state: AuthState::Authenticated,
|
auth_state: AuthState::Authenticated,
|
||||||
client_id: assigned_id,
|
client_id: assigned_id,
|
||||||
client_public_key: Some(client_bundle),
|
client_public_key: Some(client_bundle),
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
57
package.json
57
package.json
|
|
@ -1,29 +1,60 @@
|
||||||
{
|
{
|
||||||
"name": "mtp-wasm",
|
"name": "mtp",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Browser WASM interface for MTP.",
|
"description": "MTP TypeScript SDK",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "./wasm/pkg/mtp_wasm.js",
|
"packageManager": "pnpm@11.8.0",
|
||||||
"types": "./wasm/pkg/mtp_wasm.d.ts",
|
"module": "./dist/sdk/index.js",
|
||||||
|
"types": "./dist/sdk/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./wasm/pkg/mtp_wasm.d.ts",
|
"types": "./dist/sdk/index.d.ts",
|
||||||
"import": "./wasm/pkg/mtp_wasm.js",
|
"import": "./dist/sdk/index.js",
|
||||||
"default": "./wasm/pkg/mtp_wasm.js"
|
"default": "./dist/sdk/index.js"
|
||||||
|
},
|
||||||
|
"./raw": {
|
||||||
|
"types": "./dist/raw/index.d.ts",
|
||||||
|
"import": "./dist/raw/index.js",
|
||||||
|
"default": "./dist/raw/index.js"
|
||||||
|
},
|
||||||
|
"./vite": {
|
||||||
|
"types": "./dist/vite/index.d.ts",
|
||||||
|
"import": "./dist/vite/index.js",
|
||||||
|
"default": "./dist/vite/index.js"
|
||||||
|
},
|
||||||
|
"./type-map": {
|
||||||
|
"types": "./dist/type-map/index.d.ts",
|
||||||
|
"import": "./dist/type-map/index.js",
|
||||||
|
"default": "./dist/type-map/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"wasm/pkg/mtp_wasm.js",
|
"Cargo.lock",
|
||||||
"wasm/pkg/mtp_wasm.d.ts",
|
"dist/",
|
||||||
"wasm/pkg/mtp_wasm_bg.wasm",
|
"README.md",
|
||||||
"wasm/pkg/mtp_wasm_bg.wasm.d.ts"
|
"codec/Cargo.toml",
|
||||||
|
"codec/src/",
|
||||||
|
"common/Cargo.toml",
|
||||||
|
"common/src/",
|
||||||
|
"crypto/Cargo.toml",
|
||||||
|
"crypto/src/",
|
||||||
|
"type-map/Cargo.toml",
|
||||||
|
"type-map/build.rs",
|
||||||
|
"type-map/src/",
|
||||||
|
"wasm/.cargo/",
|
||||||
|
"wasm/Cargo.toml",
|
||||||
|
"wasm/src/",
|
||||||
|
"tsconfig.json"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "MTP_TYPE_MAPS=$PWD/example/type-maps.yaml RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build wasm --target web --out-dir pkg --release",
|
"example": "pnpm install && pnpm run build:all && nix develop .#autoStart",
|
||||||
|
"build": "tsc && MTP_TYPE_MAPS=$PWD/example/type-maps.yaml RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build wasm --target web --out-dir pkg --release",
|
||||||
"build:all": "nix run .#build-all",
|
"build:all": "nix run .#build-all",
|
||||||
"dup": "jscpd --pattern '**/*.rs' --ignore 'target/**' --ignore 'wasm/pkg/**' --ignore '.git/**' --min-lines 8 --min-tokens 80 --threshold 4 --reporters console --no-tips ."
|
"dup": "jscpd --pattern '**/*.rs' --ignore 'target/**' --ignore 'wasm/pkg/**' --ignore '.git/**' --min-lines 8 --min-tokens 80 --threshold 4 --reporters console --no-tips ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jscpd": "5.0.11"
|
"@types/node": "^26.0.1",
|
||||||
|
"jscpd": "5.0.11",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
599
pnpm-lock.yaml
generated
Normal file
599
pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,599 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^26.0.1
|
||||||
|
version: 26.0.1
|
||||||
|
jscpd:
|
||||||
|
specifier: 5.0.11
|
||||||
|
version: 5.0.11
|
||||||
|
typescript:
|
||||||
|
specifier: ^6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
|
||||||
|
example/web-client:
|
||||||
|
dependencies:
|
||||||
|
mtp:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
|
devDependencies:
|
||||||
|
typescript:
|
||||||
|
specifier: ^6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
vite:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0(@types/node@26.0.1)
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@emnapi/core@1.11.1':
|
||||||
|
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.1':
|
||||||
|
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.2':
|
||||||
|
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.6':
|
||||||
|
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emnapi/core': ^1.7.1
|
||||||
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
|
'@oxc-project/types@0.137.0':
|
||||||
|
resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.1.3':
|
||||||
|
resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.1.3':
|
||||||
|
resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.1.3':
|
||||||
|
resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.1.3':
|
||||||
|
resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
|
||||||
|
resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.1.3':
|
||||||
|
resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.1.3':
|
||||||
|
resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
|
||||||
|
resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.1.3':
|
||||||
|
resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.1.3':
|
||||||
|
resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.1.3':
|
||||||
|
resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.1.3':
|
||||||
|
resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.1.3':
|
||||||
|
resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.1.3':
|
||||||
|
resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.1.3':
|
||||||
|
resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.1':
|
||||||
|
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.3':
|
||||||
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
|
'@types/node@26.0.1':
|
||||||
|
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
|
||||||
|
|
||||||
|
cpd-darwin-arm64@5.0.11:
|
||||||
|
resolution: {integrity: sha512-3QvH+4Dv7A7esVFM2tsRVWN3kn9EDu8dMYog6gYAVsCtxEf4xyxAwS/ef6LjC7/dh4+ATADFbg3H09A2fD//Qw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
cpd-darwin-x64@5.0.11:
|
||||||
|
resolution: {integrity: sha512-OvgM2ps0OFR5jUzx7+FK9URdJGxUzzM5KKk2F1V3vf1LooGDKwkivfIDyKsqEwp37zcbyUo7COvBpJXOT0dZmQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
cpd-linux-arm64-gnu@5.0.11:
|
||||||
|
resolution: {integrity: sha512-pXMINibAeruglni8ZajlXEefZHDs7QFSG+vPtkBDu7uiIMpNU8aoktgO2vP+PbIRFD0vHkqTMb64kDtIOqQcwQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
cpd-linux-x64-gnu@5.0.11:
|
||||||
|
resolution: {integrity: sha512-rQ7DuF0lH1HLzjGxlE0aEP2ycfhXgZH/CLSeS7FXNJ38lRVp+iXkrlcrrY6mC4WW/NgbL+DkF7/0lv3tFvGmvg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
cpd-linux-x64-musl@5.0.11:
|
||||||
|
resolution: {integrity: sha512-Yh+7Go5+fA++I5ssAZg7gUkDCT5CxnzCPvrspbwDrfnwaY6nNM5g1C6Vs0+GJhsspuAKwydJl4nf7jkxzMwRQw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
cpd-windows-x64-msvc@5.0.11:
|
||||||
|
resolution: {integrity: sha512-uV6w85qdfE0WJsrLcGw9A4Kv9ovSnlXZCybMK0esvuiJ7clgaZmDiDPozt5PvrOOShkK/NxzTZSORBSdVnquHA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
fdir@6.5.0:
|
||||||
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
picomatch: ^3 || ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
picomatch:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
jscpd@5.0.11:
|
||||||
|
resolution: {integrity: sha512-NfLrFJHRM6rIf3oVcdZ4sfhMVop1qxi5r8aC99lpj55YC8hiWaN4VmzU2wcXTwoAo+NS4npXPs9EVEQJ6jyRlg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||||
|
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.32.0:
|
||||||
|
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
|
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
|
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
|
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.32.0:
|
||||||
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
nanoid@3.3.15:
|
||||||
|
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
picomatch@4.0.4:
|
||||||
|
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postcss@8.5.15:
|
||||||
|
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
rolldown@1.1.3:
|
||||||
|
resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tinyglobby@0.2.17:
|
||||||
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
typescript@6.0.3:
|
||||||
|
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@8.3.0:
|
||||||
|
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
|
||||||
|
|
||||||
|
vite@8.1.0:
|
||||||
|
resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^20.19.0 || >=22.12.0
|
||||||
|
'@vitejs/devtools': ^0.3.0
|
||||||
|
esbuild: ^0.27.0 || ^0.28.0
|
||||||
|
jiti: '>=1.21.0'
|
||||||
|
less: ^4.0.0
|
||||||
|
sass: ^1.70.0
|
||||||
|
sass-embedded: ^1.70.0
|
||||||
|
stylus: '>=0.54.8'
|
||||||
|
sugarss: ^5.0.0
|
||||||
|
terser: ^5.16.0
|
||||||
|
tsx: ^4.8.1
|
||||||
|
yaml: ^2.4.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitejs/devtools':
|
||||||
|
optional: true
|
||||||
|
esbuild:
|
||||||
|
optional: true
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
yaml:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@emnapi/core@1.11.1':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/wasi-threads': 1.2.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.2':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.11.1
|
||||||
|
'@emnapi/runtime': 1.11.1
|
||||||
|
'@tybys/wasm-util': 0.10.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxc-project/types@0.137.0': {}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.11.1
|
||||||
|
'@emnapi/runtime': 1.11.1
|
||||||
|
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.1.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.3':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/node@26.0.1':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 8.3.0
|
||||||
|
|
||||||
|
cpd-darwin-arm64@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cpd-darwin-x64@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cpd-linux-arm64-gnu@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cpd-linux-x64-gnu@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cpd-linux-x64-musl@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cpd-windows-x64-msvc@5.0.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
|
optionalDependencies:
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
jscpd@5.0.11:
|
||||||
|
optionalDependencies:
|
||||||
|
cpd-darwin-arm64: 5.0.11
|
||||||
|
cpd-darwin-x64: 5.0.11
|
||||||
|
cpd-linux-arm64-gnu: 5.0.11
|
||||||
|
cpd-linux-x64-gnu: 5.0.11
|
||||||
|
cpd-linux-x64-musl: 5.0.11
|
||||||
|
cpd-windows-x64-msvc: 5.0.11
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.32.0:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-android-arm64: 1.32.0
|
||||||
|
lightningcss-darwin-arm64: 1.32.0
|
||||||
|
lightningcss-darwin-x64: 1.32.0
|
||||||
|
lightningcss-freebsd-x64: 1.32.0
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.32.0
|
||||||
|
lightningcss-linux-arm64-gnu: 1.32.0
|
||||||
|
lightningcss-linux-arm64-musl: 1.32.0
|
||||||
|
lightningcss-linux-x64-gnu: 1.32.0
|
||||||
|
lightningcss-linux-x64-musl: 1.32.0
|
||||||
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
nanoid@3.3.15: {}
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
picomatch@4.0.4: {}
|
||||||
|
|
||||||
|
postcss@8.5.15:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.15
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
rolldown@1.1.3:
|
||||||
|
dependencies:
|
||||||
|
'@oxc-project/types': 0.137.0
|
||||||
|
'@rolldown/pluginutils': 1.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@rolldown/binding-android-arm64': 1.1.3
|
||||||
|
'@rolldown/binding-darwin-arm64': 1.1.3
|
||||||
|
'@rolldown/binding-darwin-x64': 1.1.3
|
||||||
|
'@rolldown/binding-freebsd-x64': 1.1.3
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf': 1.1.3
|
||||||
|
'@rolldown/binding-linux-arm64-gnu': 1.1.3
|
||||||
|
'@rolldown/binding-linux-arm64-musl': 1.1.3
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu': 1.1.3
|
||||||
|
'@rolldown/binding-linux-s390x-gnu': 1.1.3
|
||||||
|
'@rolldown/binding-linux-x64-gnu': 1.1.3
|
||||||
|
'@rolldown/binding-linux-x64-musl': 1.1.3
|
||||||
|
'@rolldown/binding-openharmony-arm64': 1.1.3
|
||||||
|
'@rolldown/binding-wasm32-wasi': 1.1.3
|
||||||
|
'@rolldown/binding-win32-arm64-msvc': 1.1.3
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.1.3
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
tinyglobby@0.2.17:
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
typescript@6.0.3: {}
|
||||||
|
|
||||||
|
undici-types@8.3.0: {}
|
||||||
|
|
||||||
|
vite@8.1.0(@types/node@26.0.1):
|
||||||
|
dependencies:
|
||||||
|
lightningcss: 1.32.0
|
||||||
|
picomatch: 4.0.4
|
||||||
|
postcss: 8.5.15
|
||||||
|
rolldown: 1.1.3
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 26.0.1
|
||||||
|
fsevents: 2.3.3
|
||||||
8
pnpm-workspace.yaml
Normal file
8
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
packages:
|
||||||
|
- .
|
||||||
|
- example/web-client
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
2
src/raw/index.ts
Normal file
2
src/raw/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "../../wasm/pkg/mtp_wasm.js";
|
||||||
|
export * from "../../wasm/pkg/mtp_wasm.js";
|
||||||
514
src/sdk/index.ts
Normal file
514
src/sdk/index.ts
Normal file
|
|
@ -0,0 +1,514 @@
|
||||||
|
import initWasm, {
|
||||||
|
ConnectionConfig,
|
||||||
|
ConnectionState,
|
||||||
|
WasmClient,
|
||||||
|
ed25519_generate,
|
||||||
|
keyring_from_ed25519,
|
||||||
|
} from "mtp/raw";
|
||||||
|
import * as bindings from "mtp/raw";
|
||||||
|
import type * as RawBindings from "../raw/index";
|
||||||
|
import type { MTPCommunicationType } from "../type-map/index";
|
||||||
|
|
||||||
|
export type StorageValue = string | null;
|
||||||
|
|
||||||
|
export interface MTPCredentialStorage {
|
||||||
|
getItem(key: string): StorageValue | Promise<StorageValue>;
|
||||||
|
setItem(key: string, value: string): void | Promise<void>;
|
||||||
|
removeItem(key: string): void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MTPStorage = MTPCredentialStorage;
|
||||||
|
|
||||||
|
export type MTPLogEvent =
|
||||||
|
| { hint: "info" | "warning"; type: string; data: unknown }
|
||||||
|
| { hint: "error"; type: string | "error"; error: string };
|
||||||
|
|
||||||
|
export type ParsedFrame = RawBindings.ParsedFrame;
|
||||||
|
|
||||||
|
export interface MTPCredentials {
|
||||||
|
clientId: bigint | string | number | null;
|
||||||
|
keyring: Uint8Array | number[];
|
||||||
|
/** @deprecated Use keyring. Kept as a migration alias for existing callers. */
|
||||||
|
keyringBytes?: Uint8Array | number[];
|
||||||
|
hostPublicKey?: Uint8Array | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MTPClientOptions {
|
||||||
|
url: string;
|
||||||
|
hostPublicKey?: Uint8Array | string;
|
||||||
|
credentials?: MTPCredentials | string | null;
|
||||||
|
credentialsStorageKey?: string;
|
||||||
|
storage?: MTPCredentialStorage;
|
||||||
|
serverCertificateHashes?: string[];
|
||||||
|
pings?: boolean | { intervalMs?: number };
|
||||||
|
wasm?: RawBindings.InitInput | Promise<RawBindings.InitInput> | { module_or_path: RawBindings.InitInput | Promise<RawBindings.InitInput> };
|
||||||
|
logger?: (event: MTPLogEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Unsubscribe = () => void;
|
||||||
|
|
||||||
|
export interface MTPSendOptions {
|
||||||
|
id?: number;
|
||||||
|
sender?: bigint | number;
|
||||||
|
receiver?: bigint | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MTPRequestOptions extends MTPSendOptions {
|
||||||
|
responseType?: MTPCommunicationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalCredentials = Omit<MTPCredentials, "clientId" | "keyring" | "hostPublicKey"> & {
|
||||||
|
clientId: bigint | null;
|
||||||
|
keyringBytes: Uint8Array;
|
||||||
|
hostPublicKey?: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NormalizedMTPClientOptions = Omit<MTPClientOptions, "hostPublicKey"> & {
|
||||||
|
hostPublicKey?: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_CREDENTIALS_KEY = "mtp:credentials";
|
||||||
|
|
||||||
|
function emit(logger, event) {
|
||||||
|
if (typeof logger === "function") {
|
||||||
|
logger(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isErrorType(type) {
|
||||||
|
return type === "Error" || type.startsWith("Error") || [
|
||||||
|
"BadRequest",
|
||||||
|
"Unauthorized",
|
||||||
|
"Forbidden",
|
||||||
|
"NotFound",
|
||||||
|
"TooManyRequests",
|
||||||
|
"InternalServerError",
|
||||||
|
"BadGateway",
|
||||||
|
"ServiceUnavailable",
|
||||||
|
"GatewayTimeout",
|
||||||
|
].includes(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(frame) {
|
||||||
|
const data = frame?.data ?? {};
|
||||||
|
return String(data.ErrorMessage ?? data.Error ?? data.Description ?? `Received ${frame?.type ?? "error"} frame`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storageGet(storage, key) {
|
||||||
|
return storage ? await storage.getItem(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storageSet(storage, key, value) {
|
||||||
|
if (storage) {
|
||||||
|
await storage.setItem(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storageRemove(storage, key) {
|
||||||
|
if (storage) {
|
||||||
|
await storage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBytes(value) {
|
||||||
|
return value instanceof Uint8Array || Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesFrom(value, name) {
|
||||||
|
if (value instanceof Uint8Array) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return new Uint8Array(value);
|
||||||
|
}
|
||||||
|
throw new TypeError(`${name} must be a Uint8Array or number[]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesFromString(value, name) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new TypeError(`${name} must not be empty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hex = trimmed.replace(/^(0x)/i, "").replace(/[\s:_-]/g, "");
|
||||||
|
if (/^[0-9a-fA-F]+$/.test(hex)) {
|
||||||
|
if (hex.length % 2 !== 0) {
|
||||||
|
throw new TypeError(`${name} hex string has an odd length`);
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < bytes.length; i += 1) {
|
||||||
|
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof atob === "function") {
|
||||||
|
const binary = atob(trimmed);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i += 1) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
return new Uint8Array(Buffer.from(trimmed, "base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`${name} must be bytes, hex, or base64`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBytes(value, name) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return bytesFromString(value, name);
|
||||||
|
}
|
||||||
|
return bytesFrom(value, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCredentials(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBigInt(value) {
|
||||||
|
if (value == null || value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return typeof value === "bigint" ? value : BigInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateKeyringBytes() {
|
||||||
|
const generated = ed25519_generate();
|
||||||
|
try {
|
||||||
|
return keyring_from_ed25519(generated.secretKey, generated.publicKey);
|
||||||
|
} finally {
|
||||||
|
generated.signer?.free?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeCredentials(credentials) {
|
||||||
|
return JSON.stringify({
|
||||||
|
clientId: credentials.clientId?.toString() ?? null,
|
||||||
|
keyring: Array.from(credentials.keyringBytes ?? []),
|
||||||
|
hostPublicKey: credentials.hostPublicKey ? Array.from(credentials.hostPublicKey) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeCredentials(credentials) {
|
||||||
|
const normalized = normalizeCredentials(credentials);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyring = normalized.keyring ?? normalized.keyringBytes;
|
||||||
|
if (!isBytes(keyring)) {
|
||||||
|
throw new TypeError("credentials.keyring must be a Uint8Array or number[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: toBigInt(normalized.clientId),
|
||||||
|
keyringBytes: bytesFrom(keyring, "credentials.keyring"),
|
||||||
|
hostPublicKey: normalized.hostPublicKey == null
|
||||||
|
? undefined
|
||||||
|
: normalizeBytes(normalized.hostPublicKey, "credentials.hostPublicKey"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicCredentials(credentials) {
|
||||||
|
if (!credentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clientId: credentials.clientId,
|
||||||
|
keyring: credentials.keyringBytes,
|
||||||
|
keyringBytes: credentials.keyringBytes,
|
||||||
|
hostPublicKey: credentials.hostPublicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptions(options) {
|
||||||
|
if (!options || typeof options !== "object") {
|
||||||
|
throw new TypeError("MTPClient.create requires an options object");
|
||||||
|
}
|
||||||
|
if (typeof options.url !== "string" || !options.url.trim()) {
|
||||||
|
throw new TypeError("MTPClient.create requires a non-empty url");
|
||||||
|
}
|
||||||
|
if (options.storage) {
|
||||||
|
for (const method of ["getItem", "setItem", "removeItem"]) {
|
||||||
|
if (typeof options.storage[method] !== "function") {
|
||||||
|
throw new TypeError(`storage.${method} must be a function`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MTPClient {
|
||||||
|
#credentials: InternalCredentials | null;
|
||||||
|
#options: NormalizedMTPClientOptions;
|
||||||
|
readonly raw: {
|
||||||
|
client: RawBindings.WasmClient;
|
||||||
|
bindings: typeof RawBindings;
|
||||||
|
};
|
||||||
|
|
||||||
|
private constructor(options: NormalizedMTPClientOptions, client: RawBindings.WasmClient) {
|
||||||
|
this.#options = options;
|
||||||
|
this.#credentials = deserializeCredentials(options.credentials);
|
||||||
|
this.raw = { client, bindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(options: MTPClientOptions = {} as MTPClientOptions): Promise<MTPClient> {
|
||||||
|
validateOptions(options);
|
||||||
|
await initWasm(options.wasm);
|
||||||
|
|
||||||
|
const normalizedOptions = {
|
||||||
|
...options,
|
||||||
|
hostPublicKey: options.hostPublicKey == null
|
||||||
|
? undefined
|
||||||
|
: normalizeBytes(options.hostPublicKey, "hostPublicKey"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sdk: MTPClient | undefined;
|
||||||
|
const client = new WasmClient(
|
||||||
|
(state) => emit(normalizedOptions.logger, {
|
||||||
|
hint: "info",
|
||||||
|
type: "state",
|
||||||
|
data: ConnectionState[state] ?? state,
|
||||||
|
}),
|
||||||
|
(frame) => {
|
||||||
|
if (sdk) {
|
||||||
|
sdk.#handleFrame(frame);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => emit(normalizedOptions.logger, {
|
||||||
|
hint: "error",
|
||||||
|
type: "error",
|
||||||
|
error: String(error),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
sdk = new MTPClient(normalizedOptions, client);
|
||||||
|
await sdk.#loadStoredCredentials();
|
||||||
|
if (!sdk.#credentials) {
|
||||||
|
sdk.#credentials = {
|
||||||
|
clientId: null,
|
||||||
|
keyringBytes: generateKeyringBytes(),
|
||||||
|
hostPublicKey: normalizedOptions.hostPublicKey,
|
||||||
|
};
|
||||||
|
} else if (!sdk.#credentials.hostPublicKey && normalizedOptions.hostPublicKey) {
|
||||||
|
sdk.#credentials = { ...sdk.#credentials, hostPublicKey: normalizedOptions.hostPublicKey };
|
||||||
|
} else if (!normalizedOptions.hostPublicKey && sdk.#credentials.hostPublicKey) {
|
||||||
|
sdk.#options = { ...sdk.#options, hostPublicKey: sdk.#credentials.hostPublicKey };
|
||||||
|
}
|
||||||
|
return sdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return WasmClient.is_supported();
|
||||||
|
}
|
||||||
|
|
||||||
|
get credentials(): MTPCredentials | null {
|
||||||
|
return publicCredentials(this.#credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #loadStoredCredentials() {
|
||||||
|
if (this.#credentials || !this.#options.storage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = await storageGet(
|
||||||
|
this.#options.storage,
|
||||||
|
this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY,
|
||||||
|
);
|
||||||
|
this.#credentials = deserializeCredentials(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectionConfig() {
|
||||||
|
const config = new ConnectionConfig(this.#options.url);
|
||||||
|
if (this.#options.serverCertificateHashes) {
|
||||||
|
config.server_certificate_hashes = this.#options.serverCertificateHashes;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
if (this.#credentials?.clientId != null && this.#options.hostPublicKey) {
|
||||||
|
await this.#connectAuthenticated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.#connectionConfig();
|
||||||
|
try {
|
||||||
|
await this.raw.client.connect(config);
|
||||||
|
this.#startPings(0n);
|
||||||
|
} finally {
|
||||||
|
config.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #connectAuthenticated() {
|
||||||
|
if (!this.#options.hostPublicKey) {
|
||||||
|
throw new Error("MTPClient.connect requires hostPublicKey for authenticated connections");
|
||||||
|
}
|
||||||
|
if (!this.#credentials?.keyringBytes?.length || this.#credentials.clientId == null) {
|
||||||
|
throw new Error("MTPClient.connect requires credentials with clientId and keyring");
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.#connectionConfig();
|
||||||
|
try {
|
||||||
|
const clientId = await this.raw.client.auth_connect(
|
||||||
|
config,
|
||||||
|
this.#options.hostPublicKey,
|
||||||
|
this.#credentials.keyringBytes,
|
||||||
|
this.#credentials.clientId,
|
||||||
|
);
|
||||||
|
this.#credentials = { ...this.#credentials, clientId };
|
||||||
|
await this.#persistCredentials();
|
||||||
|
this.#startPings(clientId);
|
||||||
|
return clientId;
|
||||||
|
} finally {
|
||||||
|
config.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(): Promise<bigint> {
|
||||||
|
if (!this.#options.hostPublicKey) {
|
||||||
|
throw new Error("MTPClient.register requires hostPublicKey");
|
||||||
|
}
|
||||||
|
if (!this.#credentials?.keyringBytes?.length) {
|
||||||
|
this.#credentials = {
|
||||||
|
clientId: null,
|
||||||
|
keyringBytes: generateKeyringBytes(),
|
||||||
|
hostPublicKey: this.#options.hostPublicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.#connectionConfig();
|
||||||
|
try {
|
||||||
|
const clientId = await this.raw.client.auth_register(
|
||||||
|
config,
|
||||||
|
this.#options.hostPublicKey,
|
||||||
|
this.#credentials.keyringBytes,
|
||||||
|
);
|
||||||
|
this.#credentials = { ...this.#credentials, clientId };
|
||||||
|
await this.#persistCredentials();
|
||||||
|
this.#startPings(clientId);
|
||||||
|
return clientId;
|
||||||
|
} finally {
|
||||||
|
config.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectOrRegister(): Promise<bigint> {
|
||||||
|
return this.#credentials?.clientId == null
|
||||||
|
? await this.register()
|
||||||
|
: await this.#connectAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #persistCredentials() {
|
||||||
|
await storageSet(
|
||||||
|
this.#options.storage,
|
||||||
|
this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY,
|
||||||
|
serializeCredentials(this.#credentials),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCredentials(): Promise<void> {
|
||||||
|
this.#credentials = null;
|
||||||
|
await storageRemove(
|
||||||
|
this.#options.storage,
|
||||||
|
this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#startPings(clientId) {
|
||||||
|
const pings = this.#options.pings;
|
||||||
|
if (!pings) {
|
||||||
|
this.raw.client.stop_protocol_pings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const intervalMs = typeof pings === "object" ? pings.intervalMs ?? 30_000 : 30_000;
|
||||||
|
this.raw.client.start_protocol_pings(intervalMs, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildFrame(typeOrFrame, data, options) {
|
||||||
|
if (typeOrFrame instanceof Uint8Array) {
|
||||||
|
return typeOrFrame;
|
||||||
|
}
|
||||||
|
if (typeof typeOrFrame !== "string" || !typeOrFrame) {
|
||||||
|
throw new TypeError("message type must be a non-empty string or Uint8Array frame");
|
||||||
|
}
|
||||||
|
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
||||||
|
throw new TypeError("message data must be an object");
|
||||||
|
}
|
||||||
|
return this.raw.bindings.build_frame(typeOrFrame, data, options ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: Uint8Array): Promise<void>;
|
||||||
|
async send(type: MTPCommunicationType, data: Record<string, unknown>, options?: MTPSendOptions): Promise<void>;
|
||||||
|
async send(typeOrFrame: Uint8Array | MTPCommunicationType, data?: Record<string, unknown>, options?: MTPSendOptions): Promise<void> {
|
||||||
|
const message = this.#buildFrame(typeOrFrame, data, options);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frame = this.raw.bindings.parse_frame(message);
|
||||||
|
emit(this.#options.logger, isErrorType(frame.type)
|
||||||
|
? { hint: "error", type: frame.type, error: errorMessage(frame) }
|
||||||
|
: { hint: "info", type: frame.type, data: frame.data });
|
||||||
|
} catch (error) {
|
||||||
|
emit(this.#options.logger, {
|
||||||
|
hint: "error",
|
||||||
|
type: "error",
|
||||||
|
error: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.raw.client.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(message: Uint8Array, data?: never, options?: MTPRequestOptions): Promise<ParsedFrame>;
|
||||||
|
async request(type: MTPCommunicationType, data: Record<string, unknown>, options?: MTPRequestOptions): Promise<ParsedFrame>;
|
||||||
|
async request(typeOrFrame: Uint8Array | MTPCommunicationType, data?: Record<string, unknown>, options: MTPRequestOptions = {}): Promise<ParsedFrame> {
|
||||||
|
const frame = this.#buildFrame(typeOrFrame, data, options);
|
||||||
|
return await this.raw.client.request(frame, options.responseType ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(type: MTPCommunicationType, handler: (message: ParsedFrame) => void): Unsubscribe {
|
||||||
|
if (typeof type !== "string" || !type) {
|
||||||
|
throw new TypeError("subscription type must be a non-empty string");
|
||||||
|
}
|
||||||
|
if (typeof handler !== "function") {
|
||||||
|
throw new TypeError("subscription handler must be a function");
|
||||||
|
}
|
||||||
|
const id = this.raw.client.subscribe(type, handler);
|
||||||
|
return () => this.raw.client.unsubscribe(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleFrame(frame) {
|
||||||
|
if (isErrorType(frame.type)) {
|
||||||
|
emit(this.#options.logger, {
|
||||||
|
hint: "error",
|
||||||
|
type: frame.type,
|
||||||
|
error: errorMessage(frame),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
emit(this.#options.logger, {
|
||||||
|
hint: "info",
|
||||||
|
type: frame.type,
|
||||||
|
data: frame.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.raw.client.stop_protocol_pings();
|
||||||
|
this.raw.client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { bindings as raw };
|
||||||
5
src/type-map/index.ts
Normal file
5
src/type-map/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type MTPCommunicationType = string;
|
||||||
|
export type MTPDataType = string;
|
||||||
|
|
||||||
|
export const communicationTypes: readonly MTPCommunicationType[] = [];
|
||||||
|
export const dataTypes: readonly MTPDataType[] = [];
|
||||||
409
src/vite/index.ts
Normal file
409
src/vite/index.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export interface MTPVitePluginOptions {
|
||||||
|
typeMaps: string;
|
||||||
|
release?: boolean;
|
||||||
|
wasmPackArgs?: string[];
|
||||||
|
outDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VitePlugin {
|
||||||
|
name: string;
|
||||||
|
config?: (...args: any[]) => unknown;
|
||||||
|
buildStart?: (...args: any[]) => unknown;
|
||||||
|
configureServer?: (...args: any[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageRoot = process.env.MTP_PACKAGE_ROOT
|
||||||
|
? path.resolve(process.env.MTP_PACKAGE_ROOT)
|
||||||
|
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
const rawEntryName = "mtp_wasm.js";
|
||||||
|
const wasmEntryName = "mtp_wasm_bg.wasm";
|
||||||
|
const typeMapEntryName = "mtp_type_map.js";
|
||||||
|
|
||||||
|
const reservedCommunicationTypes = [
|
||||||
|
"Identification",
|
||||||
|
"IdentificationResponse",
|
||||||
|
"Register",
|
||||||
|
"RegisterResponse",
|
||||||
|
"Challenge",
|
||||||
|
"ChallengeResponse",
|
||||||
|
"Ping",
|
||||||
|
"Pong",
|
||||||
|
"Disconnect",
|
||||||
|
"Redirect",
|
||||||
|
"Shutdown",
|
||||||
|
"Error",
|
||||||
|
"ErrorParsing",
|
||||||
|
"ErrorBadVersion",
|
||||||
|
"BadRequest",
|
||||||
|
"Unauthorized",
|
||||||
|
"Forbidden",
|
||||||
|
"NotFound",
|
||||||
|
"TooManyRequests",
|
||||||
|
"InternalServerError",
|
||||||
|
"BadGateway",
|
||||||
|
"ServiceUnavailable",
|
||||||
|
"GatewayTimeout",
|
||||||
|
];
|
||||||
|
|
||||||
|
const reservedDataTypes = [
|
||||||
|
"Version",
|
||||||
|
"Id",
|
||||||
|
"ClientNonce",
|
||||||
|
"ServerNonce",
|
||||||
|
"PublicKeys",
|
||||||
|
"Signature",
|
||||||
|
"PqSignature",
|
||||||
|
"Description",
|
||||||
|
"Connected",
|
||||||
|
"Timestamp",
|
||||||
|
"Error",
|
||||||
|
"ErrorParsing",
|
||||||
|
"ErrorMessage",
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeOptions(options) {
|
||||||
|
if (!options?.typeMaps) {
|
||||||
|
throw new Error("mtp/vite requires a typeMaps option, for example mtp({ typeMaps: './type-maps.yaml' })");
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function devServerPath(root: string, filePath: string) {
|
||||||
|
const relativePath = path.relative(root, filePath);
|
||||||
|
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/${relativePath.split(path.sep).join("/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashPackageInputs() {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
const inputs = [
|
||||||
|
"wasm/Cargo.toml",
|
||||||
|
"wasm/src",
|
||||||
|
"common/Cargo.toml",
|
||||||
|
"common/src",
|
||||||
|
"codec/Cargo.toml",
|
||||||
|
"codec/src",
|
||||||
|
"crypto/Cargo.toml",
|
||||||
|
"crypto/src",
|
||||||
|
"type-map/Cargo.toml",
|
||||||
|
"type-map/build.rs",
|
||||||
|
"type-map/src",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function addPath(relativePath) {
|
||||||
|
const absolutePath = path.join(packageRoot, relativePath);
|
||||||
|
const stat = await fs.stat(absolutePath).catch(() => null);
|
||||||
|
if (!stat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
const entries = await fs.readdir(absolutePath);
|
||||||
|
for (const entry of entries.sort()) {
|
||||||
|
await addPath(path.join(relativePath, entry));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash.update(relativePath);
|
||||||
|
hash.update(await fs.readFile(absolutePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const input of inputs) {
|
||||||
|
await addPath(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteList(values) {
|
||||||
|
return values.length === 0
|
||||||
|
? "never"
|
||||||
|
: values.map((value) => JSON.stringify(value)).join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTypeMapYaml(source, filePath) {
|
||||||
|
const communicationTypes = new Set(reservedCommunicationTypes);
|
||||||
|
const dataTypes = new Set(reservedDataTypes);
|
||||||
|
let section = null;
|
||||||
|
let sectionIndent = -1;
|
||||||
|
|
||||||
|
for (const [index, originalLine] of source.split(/\r?\n/).entries()) {
|
||||||
|
const withoutComment = originalLine.replace(/\s+#.*$/, "");
|
||||||
|
if (!withoutComment.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^\t/.test(withoutComment)) {
|
||||||
|
throw new Error(`${filePath}:${index + 1}: tabs are not supported in type-maps.yaml indentation`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const indent = withoutComment.match(/^ */)?.[0].length ?? 0;
|
||||||
|
const trimmed = withoutComment.trim();
|
||||||
|
const sectionMatch = trimmed.match(/^(CommunicationTypes|DataTypes):\s*$/);
|
||||||
|
if (sectionMatch) {
|
||||||
|
section = sectionMatch[1];
|
||||||
|
sectionIndent = indent;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section && indent <= sectionIndent) {
|
||||||
|
section = null;
|
||||||
|
}
|
||||||
|
if (!section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*):\s*\d+\s*$/);
|
||||||
|
if (!entryMatch) {
|
||||||
|
throw new Error(`${filePath}:${index + 1}: expected '${section}' entries as 'Name: numeric_id'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "CommunicationTypes") {
|
||||||
|
communicationTypes.add(entryMatch[1]);
|
||||||
|
} else {
|
||||||
|
dataTypes.add(entryMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
communicationTypes: [...communicationTypes].sort(),
|
||||||
|
dataTypes: [...dataTypes].sort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTypeMapModule(metadata) {
|
||||||
|
const js = `export const communicationTypes = ${JSON.stringify(metadata.communicationTypes, null, 2)};\nexport const dataTypes = ${JSON.stringify(metadata.dataTypes, null, 2)};\n`;
|
||||||
|
const dts = `export type MTPCommunicationType = ${quoteList(metadata.communicationTypes)};\nexport type MTPDataType = ${quoteList(metadata.dataTypes)};\nexport declare const communicationTypes: readonly MTPCommunicationType[];\nexport declare const dataTypes: readonly MTPDataType[];\n`;
|
||||||
|
return { js, dts };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeTypeMapModule(outDir, typeMapsPath) {
|
||||||
|
const source = await fs.readFile(typeMapsPath, "utf8").catch((error) => {
|
||||||
|
throw new Error(`Failed to read type map '${typeMapsPath}': ${error.message}`);
|
||||||
|
});
|
||||||
|
const metadata = parseTypeMapYaml(source, typeMapsPath);
|
||||||
|
const module = generateTypeMapModule(metadata);
|
||||||
|
await fs.mkdir(outDir, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(outDir, typeMapEntryName), module.js);
|
||||||
|
await fs.writeFile(path.join(outDir, "mtp_type_map.d.ts"), module.dts);
|
||||||
|
await fs.writeFile(path.join(outDir, `${typeMapEntryName}.d.ts`), module.dts);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyWasmBuildInputs(buildRoot) {
|
||||||
|
const inputs = [
|
||||||
|
"Cargo.lock",
|
||||||
|
"wasm",
|
||||||
|
"common",
|
||||||
|
"codec",
|
||||||
|
"crypto",
|
||||||
|
"type-map",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const input of inputs) {
|
||||||
|
const source = path.join(packageRoot, input);
|
||||||
|
if (!await pathExists(source)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.cp(source, path.join(buildRoot, input), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runWasmPack({ outDir, typeMapsPath, release, wasmPackArgs }) {
|
||||||
|
const buildRoot = await fs.mkdtemp(path.join(os.tmpdir(), "mtp-wasm-"));
|
||||||
|
const args = [
|
||||||
|
"build",
|
||||||
|
path.join(buildRoot, "wasm"),
|
||||||
|
"--target",
|
||||||
|
"web",
|
||||||
|
"--out-dir",
|
||||||
|
outDir,
|
||||||
|
];
|
||||||
|
if (release) {
|
||||||
|
args.push("--release");
|
||||||
|
} else {
|
||||||
|
args.push("--dev");
|
||||||
|
}
|
||||||
|
args.push(...wasmPackArgs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await copyWasmBuildInputs(buildRoot);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const child = spawn("wasm-pack", args, {
|
||||||
|
cwd: buildRoot,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
MTP_TYPE_MAPS: typeMapsPath,
|
||||||
|
RUSTFLAGS: [process.env.RUSTFLAGS, "--cfg web_sys_unstable_apis"].filter(Boolean).join(" "),
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk;
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk;
|
||||||
|
});
|
||||||
|
child.on("error", (error) => {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
reject(new Error("Failed to run wasm-pack. Install wasm-pack or enter the project Nix dev shell, then retry."));
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`wasm-pack failed with exit code ${code}.\n${stdout}${stderr}`.trim()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(buildRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildIfNeeded(state, force = false) {
|
||||||
|
if (state.buildPromise) {
|
||||||
|
return state.buildPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.buildPromise = (async () => {
|
||||||
|
const typeMapSource = await writeTypeMapModule(state.outDir, state.typeMapsPath);
|
||||||
|
const packageInputs = await hashPackageInputs();
|
||||||
|
const fingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(JSON.stringify({
|
||||||
|
packageRoot,
|
||||||
|
packageInputs,
|
||||||
|
typeMapsPath: state.typeMapsPath,
|
||||||
|
typeMapSource,
|
||||||
|
release: state.release,
|
||||||
|
wasmPackArgs: state.wasmPackArgs,
|
||||||
|
}))
|
||||||
|
.digest("hex");
|
||||||
|
const stampPath = path.join(state.outDir, ".mtp-build.json");
|
||||||
|
const rawEntryPath = path.join(state.outDir, rawEntryName);
|
||||||
|
const wasmPath = path.join(state.outDir, wasmEntryName);
|
||||||
|
let previousFingerprint = null;
|
||||||
|
try {
|
||||||
|
previousFingerprint = JSON.parse(await fs.readFile(stampPath, "utf8")).fingerprint;
|
||||||
|
} catch {
|
||||||
|
previousFingerprint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && previousFingerprint === fingerprint && await pathExists(rawEntryPath) && await pathExists(wasmPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("\x1b[1m\x1b[35mmtp\x1b[0m compiling wasm... (this could take a minute)");
|
||||||
|
await runWasmPack(state);
|
||||||
|
console.log("\x1b[1m\x1b[35mmtp\x1b[0m \x1b[32mcompilation finished.\x1b[0m");
|
||||||
|
await fs.writeFile(stampPath, JSON.stringify({ fingerprint, builtAt: new Date().toISOString() }, null, 2));
|
||||||
|
})().finally(() => {
|
||||||
|
state.buildPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return state.buildPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mtp(options: MTPVitePluginOptions): VitePlugin {
|
||||||
|
const normalized = normalizeOptions(options);
|
||||||
|
const state = {
|
||||||
|
outDir: null,
|
||||||
|
typeMapsPath: null,
|
||||||
|
release: true,
|
||||||
|
wasmPackArgs: normalized.wasmPackArgs ?? [],
|
||||||
|
buildPromise: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "mtp",
|
||||||
|
async config(config, env) {
|
||||||
|
const root = path.resolve(config.root ?? process.cwd());
|
||||||
|
state.typeMapsPath = path.resolve(root, normalized.typeMaps);
|
||||||
|
state.outDir = path.resolve(root, normalized.outDir ?? path.join("node_modules", ".vite", "mtp"));
|
||||||
|
state.release = normalized.release ?? env.command === "build";
|
||||||
|
|
||||||
|
if (!await pathExists(state.typeMapsPath)) {
|
||||||
|
throw new Error(`mtp/vite could not find typeMaps file: ${state.typeMapsPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await buildIfNeeded(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolve: {
|
||||||
|
preserveSymlinks: true,
|
||||||
|
alias: {
|
||||||
|
"mtp/raw": path.join(state.outDir, rawEntryName),
|
||||||
|
"mtp/type-map": path.join(state.outDir, typeMapEntryName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
buildStart() {
|
||||||
|
this.addWatchFile(state.typeMapsPath);
|
||||||
|
},
|
||||||
|
configureServer(server) {
|
||||||
|
const wasmPath = path.join(state.outDir, wasmEntryName);
|
||||||
|
const wasmUrl = devServerPath(server.config.root, wasmPath);
|
||||||
|
if (wasmUrl) {
|
||||||
|
server.middlewares.use(async (req, res, next) => {
|
||||||
|
if (!req.url || new URL(req.url, "http://localhost").pathname !== wasmUrl) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.setHeader("Content-Type", "application/wasm");
|
||||||
|
res.end(await fs.readFile(wasmPath));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server.watcher.add(state.typeMapsPath);
|
||||||
|
server.watcher.on("change", async (changedPath) => {
|
||||||
|
if (path.resolve(changedPath) !== state.typeMapsPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await buildIfNeeded(state, true);
|
||||||
|
server.moduleGraph.invalidateAll();
|
||||||
|
server.ws.send({ type: "full-reload" });
|
||||||
|
} catch (error) {
|
||||||
|
server.config.logger.error(error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mtpVitePlugin = mtp;
|
||||||
|
export default mtpVitePlugin;
|
||||||
|
|
@ -53,6 +53,45 @@ impl Default for Policy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Policy {
|
||||||
|
pub fn with_send_mode(mut self, send_mode: SendMode) -> Self {
|
||||||
|
self.send_mode = send_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_max_message_size(mut self, max_message_size: u64) -> Self {
|
||||||
|
self.max_message_size = max_message_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_timeouts(
|
||||||
|
mut self,
|
||||||
|
open_stream_timeout: Duration,
|
||||||
|
write_timeout: Duration,
|
||||||
|
read_timeout: Duration,
|
||||||
|
) -> Self {
|
||||||
|
self.open_stream_timeout = open_stream_timeout;
|
||||||
|
self.write_timeout = write_timeout;
|
||||||
|
self.read_timeout = read_timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_keep_alive(mut self, keep_alive_interval: Option<Duration>) -> Self {
|
||||||
|
self.keep_alive_interval = keep_alive_interval;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_max_idle_timeout(mut self, max_idle_timeout: Option<Duration>) -> Self {
|
||||||
|
self.max_idle_timeout = max_idle_timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_receiver_queue_capacity(mut self, receiver_queue_capacity: usize) -> Self {
|
||||||
|
self.receiver_queue_capacity = receiver_queue_capacity;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ReceivedFrame {
|
enum ReceivedFrame {
|
||||||
Message(CommunicationValue),
|
Message(CommunicationValue),
|
||||||
ClosedByPeer,
|
ClosedByPeer,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{ConnectionHandle, Policy, Receiver, Sender};
|
use crate::{ConnectionHandle, Policy, Receiver, Sender};
|
||||||
use log;
|
|
||||||
use mtp_common::CommunicationError;
|
use mtp_common::CommunicationError;
|
||||||
use rustls::pki_types::{PrivateKeyDer, pem::PemObject};
|
use rustls::pki_types::{PrivateKeyDer, pem::PemObject};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
@ -14,13 +13,7 @@ pub struct Host {
|
||||||
|
|
||||||
impl Host {
|
impl Host {
|
||||||
pub async fn next(&mut self) -> Option<(Sender, Receiver)> {
|
pub async fn next(&mut self) -> Option<(Sender, Receiver)> {
|
||||||
log::warn!("[transport Host::next] waiting on recv...");
|
self.incoming.recv().await
|
||||||
let result = self.incoming.recv().await;
|
|
||||||
match &result {
|
|
||||||
Some(_) => log::warn!("[transport Host::next] received connection"),
|
|
||||||
None => log::warn!("[transport Host::next] incoming channel closed - sender dropped"),
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_addr(&self) -> std::net::SocketAddr {
|
pub fn local_addr(&self) -> std::net::SocketAddr {
|
||||||
|
|
@ -66,19 +59,13 @@ pub async fn host(
|
||||||
let policy = Arc::new(policy);
|
let policy = Arc::new(policy);
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
log::warn!("[transport bg task] started");
|
|
||||||
loop {
|
loop {
|
||||||
log::warn!("[transport bg task] waiting for connection...");
|
|
||||||
let incoming_session = endpoint.accept().await;
|
let incoming_session = endpoint.accept().await;
|
||||||
log::warn!("[transport bg task] got incoming session");
|
|
||||||
|
|
||||||
let request = match incoming_session.await {
|
let request = match incoming_session.await {
|
||||||
Ok(req) => {
|
Ok(req) => req,
|
||||||
log::warn!("[transport bg task] got request");
|
|
||||||
req
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("[transport bg task] incoming session error: {e}");
|
log::debug!("incoming WebTransport session failed: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -87,19 +74,15 @@ pub async fn host(
|
||||||
.accept_with_headers([("sec-webtransport-http3-draft02", "1")])
|
.accept_with_headers([("sec-webtransport-http3-draft02", "1")])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(conn) => {
|
Ok(conn) => conn,
|
||||||
log::warn!("[transport bg task] connection accepted");
|
|
||||||
conn
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("[transport bg task] accept error: {e}");
|
log::debug!("WebTransport request accept failed: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let incoming_tx = incoming_tx.clone();
|
let incoming_tx = incoming_tx.clone();
|
||||||
let policy = policy.clone();
|
let policy = policy.clone();
|
||||||
eprintln!("[transport bg task] spawning handle_connection");
|
|
||||||
tokio::spawn(handle_connection(connection, incoming_tx, policy));
|
tokio::spawn(handle_connection(connection, incoming_tx, policy));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"mtp/raw": ["src/raw/index.ts"],
|
||||||
|
"mtp/type-map": ["src/type-map/index.ts"]
|
||||||
|
},
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/raw/**/*.ts",
|
||||||
|
"src/sdk/**/*.ts",
|
||||||
|
"src/type-map/**/*.ts",
|
||||||
|
"src/vite/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -13,22 +13,7 @@ crate-type = ["cdylib"]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
web-sys = { version = "0.3", features = [
|
futures-channel = "0.3"
|
||||||
"console",
|
|
||||||
"WebTransport",
|
|
||||||
"WebTransportOptions",
|
|
||||||
"WebTransportHash",
|
|
||||||
"WebTransportBidirectionalStream",
|
|
||||||
"WebTransportCloseInfo",
|
|
||||||
"WebTransportDatagramDuplexStream",
|
|
||||||
"WebTransportError",
|
|
||||||
"WebTransportReceiveStream",
|
|
||||||
"WebTransportSendStream",
|
|
||||||
"ReadableStream",
|
|
||||||
"ReadableStreamDefaultReader",
|
|
||||||
"WritableStream",
|
|
||||||
"WritableStreamDefaultWriter",
|
|
||||||
] }
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
|
||||||
94
wasm/pkg/mtp_wasm.d.ts
vendored
94
wasm/pkg/mtp_wasm.d.ts
vendored
|
|
@ -1,6 +1,17 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export interface ParsedFrame {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
sender?: bigint;
|
||||||
|
receiver?: bigint;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
raw: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class ConnectionConfig {
|
export class ConnectionConfig {
|
||||||
free(): void;
|
free(): void;
|
||||||
[Symbol.dispose](): void;
|
[Symbol.dispose](): void;
|
||||||
|
|
@ -66,7 +77,12 @@ export class WasmClient {
|
||||||
disconnect(): void;
|
disconnect(): void;
|
||||||
static is_supported(): boolean;
|
static is_supported(): boolean;
|
||||||
constructor(on_state_change: Function, on_message: Function, on_error: Function);
|
constructor(on_state_change: Function, on_message: Function, on_error: Function);
|
||||||
|
request(frame: Uint8Array, response_type?: string | null): Promise<any>;
|
||||||
send(frame: Uint8Array): Promise<void>;
|
send(frame: Uint8Array): Promise<void>;
|
||||||
|
start_protocol_pings(interval_ms: number, client_id: bigint): void;
|
||||||
|
stop_protocol_pings(): void;
|
||||||
|
subscribe(message_type: string, callback: Function): number;
|
||||||
|
unsubscribe(id: number): boolean;
|
||||||
readonly state: number;
|
readonly state: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +121,15 @@ export class WasmKeyring {
|
||||||
to_bytes(): Uint8Array;
|
to_bytes(): Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log severity used by the public SDK when translating raw WASM events.
|
||||||
|
*/
|
||||||
|
export enum WasmLogHint {
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Error = 2,
|
||||||
|
}
|
||||||
|
|
||||||
export class WasmPublicKeyBundle {
|
export class WasmPublicKeyBundle {
|
||||||
private constructor();
|
private constructor();
|
||||||
free(): void;
|
free(): void;
|
||||||
|
|
@ -117,25 +142,27 @@ export class WasmPublicKeyBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a demo Ping frame with encrypted and signed containers
|
* Minimal message router used by higher-level SDK subscription code.
|
||||||
* (mirrors the Rust client example but uses only reserved data types).
|
|
||||||
*/
|
*/
|
||||||
export function build_demo_message(client_id: bigint, keyring_bytes: Uint8Array, host_bundle_bytes: Uint8Array): Uint8Array;
|
export class WasmSubscriptionRouter {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
dispatch(message_type: string, message: any): boolean;
|
||||||
|
constructor();
|
||||||
|
subscribe(message_type: string, callback: Function): void;
|
||||||
|
unsubscribe(message_type: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a simple Ping frame with description, timestamp, and optional data.
|
* Build a typed MTP frame using generated communication/data type names.
|
||||||
|
*/
|
||||||
|
export function build_frame(message_type: string, data: any, options: any): Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a protocol-level Ping frame with description, timestamp, and optional data.
|
||||||
*/
|
*/
|
||||||
export function build_ping_frame(client_id: bigint, description: string, timestamp: bigint, data: Uint8Array): Uint8Array;
|
export function build_ping_frame(client_id: bigint, description: string, timestamp: bigint, data: Uint8Array): Uint8Array;
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a request frame with the given communication type name, request ID, and JSON data.
|
|
||||||
*
|
|
||||||
* - `comm_type`: communication type name (e.g. "get_model", "rank_models") or PascalCase
|
|
||||||
* - `id`: request ID for response correlation
|
|
||||||
* - `json_data`: JSON-stringified request payload
|
|
||||||
*/
|
|
||||||
export function build_request_frame(comm_type: string, id: number, json_data: string): Uint8Array;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a fresh Ed25519 keypair.
|
* Generate a fresh Ed25519 keypair.
|
||||||
*
|
*
|
||||||
|
|
@ -169,9 +196,9 @@ export function main(): void;
|
||||||
export function parse_auth_response(response: Uint8Array): any;
|
export function parse_auth_response(response: Uint8Array): any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a response frame into a JSON string containing `_id`, `_type`, and data fields.
|
* Parse any MTP frame into structured JavaScript data.
|
||||||
*/
|
*/
|
||||||
export function parse_response_frame(frame: Uint8Array): string;
|
export function parse_frame(frame: Uint8Array): ParsedFrame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a 32-byte encryption key from `ikm` with `salt` and `context`.
|
* Derive a 32-byte encryption key from `ikm` with `salt` and `context`.
|
||||||
|
|
@ -197,6 +224,11 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
||||||
|
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly build_frame: (a: number, b: number, c: any, d: any) => [number, number, number, number];
|
||||||
|
readonly build_ping_frame: (a: bigint, b: number, c: number, d: bigint, e: number, f: number) => [number, number, number, number];
|
||||||
|
readonly format_frame: (a: number, b: number) => [number, number, number, number];
|
||||||
|
readonly parse_auth_response: (a: number, b: number) => [number, number, number];
|
||||||
|
readonly parse_frame: (a: number, b: number) => [number, number, number];
|
||||||
readonly __wbg_wasmchacha20poly1305_free: (a: number, b: number) => void;
|
readonly __wbg_wasmchacha20poly1305_free: (a: number, b: number) => void;
|
||||||
readonly __wbg_wasmed25519signer_free: (a: number, b: number) => void;
|
readonly __wbg_wasmed25519signer_free: (a: number, b: number) => void;
|
||||||
readonly __wbg_wasmkeyring_free: (a: number, b: number) => void;
|
readonly __wbg_wasmkeyring_free: (a: number, b: number) => void;
|
||||||
|
|
@ -222,32 +254,36 @@ export interface InitOutput {
|
||||||
readonly wasmpublickeybundle_sig_cl_public_key: (a: number) => [number, number];
|
readonly wasmpublickeybundle_sig_cl_public_key: (a: number) => [number, number];
|
||||||
readonly wasmpublickeybundle_sig_pq_public_key: (a: number) => [number, number];
|
readonly wasmpublickeybundle_sig_pq_public_key: (a: number) => [number, number];
|
||||||
readonly wasmpublickeybundle_to_bytes: (a: number) => [number, number];
|
readonly wasmpublickeybundle_to_bytes: (a: number) => [number, number];
|
||||||
readonly build_demo_message: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
||||||
readonly build_ping_frame: (a: bigint, b: number, c: number, d: bigint, e: number, f: number) => [number, number, number, number];
|
|
||||||
readonly build_request_frame: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
||||||
readonly format_frame: (a: number, b: number) => [number, number, number, number];
|
|
||||||
readonly parse_auth_response: (a: number, b: number) => [number, number, number];
|
|
||||||
readonly parse_response_frame: (a: number, b: number) => [number, number, number, number];
|
|
||||||
readonly __wbg_connectionconfig_free: (a: number, b: number) => void;
|
|
||||||
readonly __wbg_wasmclient_free: (a: number, b: number) => void;
|
readonly __wbg_wasmclient_free: (a: number, b: number) => void;
|
||||||
readonly connectionconfig_client_id: (a: number) => bigint;
|
|
||||||
readonly connectionconfig_new: (a: number, b: number) => number;
|
|
||||||
readonly connectionconfig_set_client_id: (a: number, b: bigint) => void;
|
|
||||||
readonly connectionconfig_set_server_certificate_hashes: (a: number, b: number, c: number) => void;
|
|
||||||
readonly connectionconfig_url: (a: number) => [number, number];
|
|
||||||
readonly wasmclient_auth_connect: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => any;
|
readonly wasmclient_auth_connect: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => any;
|
||||||
readonly wasmclient_auth_register: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
readonly wasmclient_auth_register: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
||||||
readonly wasmclient_connect: (a: number, b: number) => any;
|
readonly wasmclient_connect: (a: number, b: number) => any;
|
||||||
readonly wasmclient_disconnect: (a: number) => void;
|
readonly wasmclient_disconnect: (a: number) => void;
|
||||||
readonly wasmclient_is_supported: () => number;
|
readonly wasmclient_is_supported: () => number;
|
||||||
readonly wasmclient_new: (a: any, b: any, c: any) => number;
|
readonly wasmclient_new: (a: any, b: any, c: any) => number;
|
||||||
|
readonly wasmclient_request: (a: number, b: number, c: number, d: number, e: number) => any;
|
||||||
readonly wasmclient_send: (a: number, b: number, c: number) => any;
|
readonly wasmclient_send: (a: number, b: number, c: number) => any;
|
||||||
|
readonly wasmclient_start_protocol_pings: (a: number, b: number, c: bigint) => [number, number];
|
||||||
readonly wasmclient_state: (a: number) => number;
|
readonly wasmclient_state: (a: number) => number;
|
||||||
|
readonly wasmclient_stop_protocol_pings: (a: number) => void;
|
||||||
|
readonly wasmclient_subscribe: (a: number, b: number, c: number, d: any) => number;
|
||||||
|
readonly wasmclient_unsubscribe: (a: number, b: number) => number;
|
||||||
|
readonly __wbg_wasmsubscriptionrouter_free: (a: number, b: number) => void;
|
||||||
readonly main: () => void;
|
readonly main: () => void;
|
||||||
|
readonly wasmsubscriptionrouter_dispatch: (a: number, b: number, c: number, d: any) => number;
|
||||||
|
readonly wasmsubscriptionrouter_new: () => number;
|
||||||
|
readonly wasmsubscriptionrouter_subscribe: (a: number, b: number, c: number, d: any) => void;
|
||||||
|
readonly wasmsubscriptionrouter_unsubscribe: (a: number, b: number, c: number) => number;
|
||||||
|
readonly __wbg_connectionconfig_free: (a: number, b: number) => void;
|
||||||
|
readonly connectionconfig_client_id: (a: number) => bigint;
|
||||||
|
readonly connectionconfig_new: (a: number, b: number) => number;
|
||||||
|
readonly connectionconfig_set_client_id: (a: number, b: bigint) => void;
|
||||||
|
readonly connectionconfig_set_server_certificate_hashes: (a: number, b: number, c: number) => void;
|
||||||
|
readonly connectionconfig_url: (a: number) => [number, number];
|
||||||
readonly wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number];
|
readonly wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number];
|
||||||
readonly wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f: (a: number, b: number, c: any) => [number, number];
|
|
||||||
readonly wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2: (a: number, b: number, c: any) => [number, number];
|
|
||||||
readonly wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void;
|
readonly wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void;
|
||||||
|
readonly wasm_bindgen__convert__closures_____invoke__h3c511b580d027299: (a: number, b: number, c: any) => void;
|
||||||
|
readonly wasm_bindgen__convert__closures_____invoke__h99ead69e8d7f5bae: (a: number, b: number) => void;
|
||||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
readonly __wbindgen_exn_store: (a: number) => void;
|
readonly __wbindgen_exn_store: (a: number) => void;
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,19 @@ export class WasmClient {
|
||||||
WasmClientFinalization.register(this, this.__wbg_ptr, this);
|
WasmClientFinalization.register(this, this.__wbg_ptr, this);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} frame
|
||||||
|
* @param {string | null} [response_type]
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
request(frame, response_type) {
|
||||||
|
const ptr0 = passArray8ToWasm0(frame, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
var ptr1 = isLikeNone(response_type) ? 0 : passStringToWasm0(response_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.wasmclient_request(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} frame
|
* @param {Uint8Array} frame
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
|
@ -237,6 +250,16 @@ export class WasmClient {
|
||||||
const ret = wasm.wasmclient_send(this.__wbg_ptr, ptr0, len0);
|
const ret = wasm.wasmclient_send(this.__wbg_ptr, ptr0, len0);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {number} interval_ms
|
||||||
|
* @param {bigint} client_id
|
||||||
|
*/
|
||||||
|
start_protocol_pings(interval_ms, client_id) {
|
||||||
|
const ret = wasm.wasmclient_start_protocol_pings(this.__wbg_ptr, interval_ms, client_id);
|
||||||
|
if (ret[1]) {
|
||||||
|
throw takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
|
|
@ -244,6 +267,28 @@ export class WasmClient {
|
||||||
const ret = wasm.wasmclient_state(this.__wbg_ptr);
|
const ret = wasm.wasmclient_state(this.__wbg_ptr);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
stop_protocol_pings() {
|
||||||
|
wasm.wasmclient_stop_protocol_pings(this.__wbg_ptr);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} message_type
|
||||||
|
* @param {Function} callback
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
subscribe(message_type, callback) {
|
||||||
|
const ptr0 = passStringToWasm0(message_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.wasmclient_subscribe(this.__wbg_ptr, ptr0, len0, callback);
|
||||||
|
return ret >>> 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
unsubscribe(id) {
|
||||||
|
const ret = wasm.wasmclient_unsubscribe(this.__wbg_ptr, id);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Symbol.dispose) WasmClient.prototype[Symbol.dispose] = WasmClient.prototype.free;
|
if (Symbol.dispose) WasmClient.prototype[Symbol.dispose] = WasmClient.prototype.free;
|
||||||
|
|
||||||
|
|
@ -365,6 +410,16 @@ export class WasmKeyring {
|
||||||
}
|
}
|
||||||
if (Symbol.dispose) WasmKeyring.prototype[Symbol.dispose] = WasmKeyring.prototype.free;
|
if (Symbol.dispose) WasmKeyring.prototype[Symbol.dispose] = WasmKeyring.prototype.free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log severity used by the public SDK when translating raw WASM events.
|
||||||
|
* @enum {0 | 1 | 2}
|
||||||
|
*/
|
||||||
|
export const WasmLogHint = Object.freeze({
|
||||||
|
Info: 0, "0": "Info",
|
||||||
|
Warning: 1, "1": "Warning",
|
||||||
|
Error: 2, "2": "Error",
|
||||||
|
});
|
||||||
|
|
||||||
export class WasmPublicKeyBundle {
|
export class WasmPublicKeyBundle {
|
||||||
static __wrap(ptr) {
|
static __wrap(ptr) {
|
||||||
const obj = Object.create(WasmPublicKeyBundle.prototype);
|
const obj = Object.create(WasmPublicKeyBundle.prototype);
|
||||||
|
|
@ -435,29 +490,79 @@ export class WasmPublicKeyBundle {
|
||||||
if (Symbol.dispose) WasmPublicKeyBundle.prototype[Symbol.dispose] = WasmPublicKeyBundle.prototype.free;
|
if (Symbol.dispose) WasmPublicKeyBundle.prototype[Symbol.dispose] = WasmPublicKeyBundle.prototype.free;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a demo Ping frame with encrypted and signed containers
|
* Minimal message router used by higher-level SDK subscription code.
|
||||||
* (mirrors the Rust client example but uses only reserved data types).
|
*/
|
||||||
* @param {bigint} client_id
|
export class WasmSubscriptionRouter {
|
||||||
* @param {Uint8Array} keyring_bytes
|
__destroy_into_raw() {
|
||||||
* @param {Uint8Array} host_bundle_bytes
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
WasmSubscriptionRouterFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_wasmsubscriptionrouter_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} message_type
|
||||||
|
* @param {any} message
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
dispatch(message_type, message) {
|
||||||
|
const ptr0 = passStringToWasm0(message_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.wasmsubscriptionrouter_dispatch(this.__wbg_ptr, ptr0, len0, message);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
const ret = wasm.wasmsubscriptionrouter_new();
|
||||||
|
this.__wbg_ptr = ret;
|
||||||
|
WasmSubscriptionRouterFinalization.register(this, this.__wbg_ptr, this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} message_type
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
subscribe(message_type, callback) {
|
||||||
|
const ptr0 = passStringToWasm0(message_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
wasm.wasmsubscriptionrouter_subscribe(this.__wbg_ptr, ptr0, len0, callback);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} message_type
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
unsubscribe(message_type) {
|
||||||
|
const ptr0 = passStringToWasm0(message_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.wasmsubscriptionrouter_unsubscribe(this.__wbg_ptr, ptr0, len0);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Symbol.dispose) WasmSubscriptionRouter.prototype[Symbol.dispose] = WasmSubscriptionRouter.prototype.free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a typed MTP frame using generated communication/data type names.
|
||||||
|
* @param {string} message_type
|
||||||
|
* @param {any} data
|
||||||
|
* @param {any} options
|
||||||
* @returns {Uint8Array}
|
* @returns {Uint8Array}
|
||||||
*/
|
*/
|
||||||
export function build_demo_message(client_id, keyring_bytes, host_bundle_bytes) {
|
export function build_frame(message_type, data, options) {
|
||||||
const ptr0 = passArray8ToWasm0(keyring_bytes, wasm.__wbindgen_malloc);
|
const ptr0 = passStringToWasm0(message_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
const len0 = WASM_VECTOR_LEN;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
const ptr1 = passArray8ToWasm0(host_bundle_bytes, wasm.__wbindgen_malloc);
|
const ret = wasm.build_frame(ptr0, len0, data, options);
|
||||||
const len1 = WASM_VECTOR_LEN;
|
|
||||||
const ret = wasm.build_demo_message(client_id, ptr0, len0, ptr1, len1);
|
|
||||||
if (ret[3]) {
|
if (ret[3]) {
|
||||||
throw takeFromExternrefTable0(ret[2]);
|
throw takeFromExternrefTable0(ret[2]);
|
||||||
}
|
}
|
||||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||||
return v3;
|
return v2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a simple Ping frame with description, timestamp, and optional data.
|
* Build a protocol-level Ping frame with description, timestamp, and optional data.
|
||||||
* @param {bigint} client_id
|
* @param {bigint} client_id
|
||||||
* @param {string} description
|
* @param {string} description
|
||||||
* @param {bigint} timestamp
|
* @param {bigint} timestamp
|
||||||
|
|
@ -478,31 +583,6 @@ export function build_ping_frame(client_id, description, timestamp, data) {
|
||||||
return v3;
|
return v3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a request frame with the given communication type name, request ID, and JSON data.
|
|
||||||
*
|
|
||||||
* - `comm_type`: communication type name (e.g. "get_model", "rank_models") or PascalCase
|
|
||||||
* - `id`: request ID for response correlation
|
|
||||||
* - `json_data`: JSON-stringified request payload
|
|
||||||
* @param {string} comm_type
|
|
||||||
* @param {number} id
|
|
||||||
* @param {string} json_data
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
export function build_request_frame(comm_type, id, json_data) {
|
|
||||||
const ptr0 = passStringToWasm0(comm_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len0 = WASM_VECTOR_LEN;
|
|
||||||
const ptr1 = passStringToWasm0(json_data, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len1 = WASM_VECTOR_LEN;
|
|
||||||
const ret = wasm.build_request_frame(ptr0, len0, id, ptr1, len1);
|
|
||||||
if (ret[3]) {
|
|
||||||
throw takeFromExternrefTable0(ret[2]);
|
|
||||||
}
|
|
||||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
||||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
||||||
return v3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a fresh Ed25519 keypair.
|
* Generate a fresh Ed25519 keypair.
|
||||||
*
|
*
|
||||||
|
|
@ -605,29 +685,18 @@ export function parse_auth_response(response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a response frame into a JSON string containing `_id`, `_type`, and data fields.
|
* Parse any MTP frame into structured JavaScript data.
|
||||||
* @param {Uint8Array} frame
|
* @param {Uint8Array} frame
|
||||||
* @returns {string}
|
* @returns {ParsedFrame}
|
||||||
*/
|
*/
|
||||||
export function parse_response_frame(frame) {
|
export function parse_frame(frame) {
|
||||||
let deferred3_0;
|
const ptr0 = passArray8ToWasm0(frame, wasm.__wbindgen_malloc);
|
||||||
let deferred3_1;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
try {
|
const ret = wasm.parse_frame(ptr0, len0);
|
||||||
const ptr0 = passArray8ToWasm0(frame, wasm.__wbindgen_malloc);
|
if (ret[2]) {
|
||||||
const len0 = WASM_VECTOR_LEN;
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
const ret = wasm.parse_response_frame(ptr0, len0);
|
|
||||||
var ptr2 = ret[0];
|
|
||||||
var len2 = ret[1];
|
|
||||||
if (ret[3]) {
|
|
||||||
ptr2 = 0; len2 = 0;
|
|
||||||
throw takeFromExternrefTable0(ret[2]);
|
|
||||||
}
|
|
||||||
deferred3_0 = ptr2;
|
|
||||||
deferred3_1 = len2;
|
|
||||||
return getStringFromWasm0(ptr2, len2);
|
|
||||||
} finally {
|
|
||||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
||||||
}
|
}
|
||||||
|
return takeFromExternrefTable0(ret[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -707,6 +776,10 @@ export function wasm_sha256_double(data) {
|
||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const import0 = {
|
const import0 = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
|
__wbg_BigInt_ff69cca7a537413a: function(arg0, arg1) {
|
||||||
|
const ret = BigInt(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg___wbindgen_boolean_get_fa956cfa2d1bd751: function(arg0) {
|
__wbg___wbindgen_boolean_get_fa956cfa2d1bd751: function(arg0) {
|
||||||
const v = arg0;
|
const v = arg0;
|
||||||
const ret = typeof(v) === 'boolean' ? v : undefined;
|
const ret = typeof(v) === 'boolean' ? v : undefined;
|
||||||
|
|
@ -740,6 +813,12 @@ function __wbg_get_imports() {
|
||||||
const ret = arg0 === undefined;
|
const ret = arg0 === undefined;
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
__wbg___wbindgen_number_get_394265ed1e1b84ee: function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||||
|
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
},
|
||||||
__wbg___wbindgen_string_get_b0ca35b86a603356: function(arg0, arg1) {
|
__wbg___wbindgen_string_get_b0ca35b86a603356: function(arg0, arg1) {
|
||||||
const obj = arg1;
|
const obj = arg1;
|
||||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||||
|
|
@ -751,6 +830,10 @@ function __wbg_get_imports() {
|
||||||
__wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
|
__wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
|
||||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
},
|
},
|
||||||
|
__wbg___wbindgen_typeof_b1bf2ff71f77b13e: function(arg0) {
|
||||||
|
const ret = typeof arg0;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg__wbg_cb_unref_fffb441def202758: function(arg0) {
|
__wbg__wbg_cb_unref_fffb441def202758: function(arg0) {
|
||||||
arg0._wbg_cb_unref();
|
arg0._wbg_cb_unref();
|
||||||
},
|
},
|
||||||
|
|
@ -762,21 +845,18 @@ function __wbg_get_imports() {
|
||||||
const ret = arg0.call(arg1, arg2);
|
const ret = arg0.call(arg1, arg2);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_close_23b01d38b065688a: function(arg0, arg1) {
|
__wbg_call_e3b662382210db98: function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
arg0.close(arg1);
|
const ret = arg0.call(arg1, arg2, arg3);
|
||||||
},
|
|
||||||
__wbg_createUnidirectionalStream_9ff0e4127f40ed0d: function(arg0) {
|
|
||||||
const ret = arg0.createUnidirectionalStream();
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
}, arguments); },
|
||||||
|
__wbg_construct_4e1a16de27aea5b9: function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = Reflect.construct(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
__wbg_crypto_38df2bab126b63dc: function(arg0) {
|
__wbg_crypto_38df2bab126b63dc: function(arg0) {
|
||||||
const ret = arg0.crypto;
|
const ret = arg0.crypto;
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_entries_015dc610cd81ede0: function(arg0) {
|
|
||||||
const ret = Object.entries(arg0);
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
__wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
|
__wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
|
||||||
let deferred0_0;
|
let deferred0_0;
|
||||||
let deferred0_1;
|
let deferred0_1;
|
||||||
|
|
@ -788,32 +868,56 @@ function __wbg_get_imports() {
|
||||||
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
__wbg_from_13e323c65fc8f464: function(arg0) {
|
||||||
|
const ret = Array.from(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) {
|
__wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) {
|
||||||
arg0.getRandomValues(arg1);
|
arg0.getRandomValues(arg1);
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_getRandomValues_cc7f052a444bb2ce: function() { return handleError(function (arg0, arg1) {
|
__wbg_getRandomValues_cc7f052a444bb2ce: function() { return handleError(function (arg0, arg1) {
|
||||||
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_get_507a50627bffa49b: function(arg0, arg1) {
|
|
||||||
const ret = arg0[arg1 >>> 0];
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
__wbg_get_78f252d074a84d0b: function() { return handleError(function (arg0, arg1) {
|
__wbg_get_78f252d074a84d0b: function() { return handleError(function (arg0, arg1) {
|
||||||
const ret = Reflect.get(arg0, arg1);
|
const ret = Reflect.get(arg0, arg1);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
|
__wbg_get_unchecked_6e0ad6d2a41b06f6: function(arg0, arg1) {
|
||||||
|
const ret = arg0[arg1 >>> 0];
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg_has_8374cf06984d8bfc: function() { return handleError(function (arg0, arg1) {
|
__wbg_has_8374cf06984d8bfc: function() { return handleError(function (arg0, arg1) {
|
||||||
const ret = Reflect.has(arg0, arg1);
|
const ret = Reflect.has(arg0, arg1);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_incomingUnidirectionalStreams_505a67efefd669d3: function(arg0) {
|
__wbg_instanceof_Promise_4cb210c0b8f8c959: function(arg0) {
|
||||||
const ret = arg0.incomingUnidirectionalStreams;
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof Promise;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_instanceof_Uint8Array_309b927aaf7a3fc7: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof Uint8Array;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_isArray_0677c962b281d01a: function(arg0) {
|
__wbg_isArray_0677c962b281d01a: function(arg0) {
|
||||||
const ret = Array.isArray(arg0);
|
const ret = Array.isArray(arg0);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
__wbg_keys_58421f8f96795607: function(arg0) {
|
||||||
|
const ret = Object.keys(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg_length_1f0964f4a5e2c6d8: function(arg0) {
|
__wbg_length_1f0964f4a5e2c6d8: function(arg0) {
|
||||||
const ret = arg0.length;
|
const ret = arg0.length;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -822,9 +926,6 @@ function __wbg_get_imports() {
|
||||||
const ret = arg0.length;
|
const ret = arg0.length;
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_log_d267660666346fb3: function(arg0) {
|
|
||||||
console.log(arg0);
|
|
||||||
},
|
|
||||||
__wbg_msCrypto_bd5a034af96bcba6: function(arg0) {
|
__wbg_msCrypto_bd5a034af96bcba6: function(arg0) {
|
||||||
const ret = arg0.msCrypto;
|
const ret = arg0.msCrypto;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -833,10 +934,10 @@ function __wbg_get_imports() {
|
||||||
const ret = new Error();
|
const ret = new Error();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_new_a2778e1fb014b494: function() { return handleError(function (arg0, arg1) {
|
__wbg_new_32b398fb48b6d94a: function() {
|
||||||
const ret = new WebTransport(getStringFromWasm0(arg0, arg1));
|
const ret = new Array();
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments); },
|
},
|
||||||
__wbg_new_cd45aabdf6073e84: function(arg0) {
|
__wbg_new_cd45aabdf6073e84: function(arg0) {
|
||||||
const ret = new Uint8Array(arg0);
|
const ret = new Uint8Array(arg0);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -871,10 +972,6 @@ function __wbg_get_imports() {
|
||||||
const ret = new Uint8Array(arg0 >>> 0);
|
const ret = new Uint8Array(arg0 >>> 0);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_new_with_options_5b1cc213336d0b4c: function() { return handleError(function (arg0, arg1, arg2) {
|
|
||||||
const ret = new WebTransport(getStringFromWasm0(arg0, arg1), arg2);
|
|
||||||
return ret;
|
|
||||||
}, arguments); },
|
|
||||||
__wbg_node_84ea875411254db1: function(arg0) {
|
__wbg_node_84ea875411254db1: function(arg0) {
|
||||||
const ret = arg0.node;
|
const ret = arg0.node;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -883,10 +980,6 @@ function __wbg_get_imports() {
|
||||||
const ret = Date.now();
|
const ret = Date.now();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbg_parse_1c0d8a8656d7e016: function() { return handleError(function (arg0, arg1) {
|
|
||||||
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
|
|
||||||
return ret;
|
|
||||||
}, arguments); },
|
|
||||||
__wbg_process_44c7a14e11e9f69e: function(arg0) {
|
__wbg_process_44c7a14e11e9f69e: function(arg0) {
|
||||||
const ret = arg0.process;
|
const ret = arg0.process;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -894,6 +987,10 @@ function __wbg_get_imports() {
|
||||||
__wbg_prototypesetcall_4770620bbe4688a0: function(arg0, arg1, arg2) {
|
__wbg_prototypesetcall_4770620bbe4688a0: function(arg0, arg1, arg2) {
|
||||||
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
||||||
},
|
},
|
||||||
|
__wbg_push_d2ae3af0c1217ae6: function(arg0, arg1) {
|
||||||
|
const ret = arg0.push(arg1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbg_queueMicrotask_0ab5b2d2393e99b9: function(arg0) {
|
__wbg_queueMicrotask_0ab5b2d2393e99b9: function(arg0) {
|
||||||
const ret = arg0.queueMicrotask;
|
const ret = arg0.queueMicrotask;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -904,10 +1001,6 @@ function __wbg_get_imports() {
|
||||||
__wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) {
|
__wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) {
|
||||||
arg0.randomFillSync(arg1);
|
arg0.randomFillSync(arg1);
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_ready_e4dad560377c42e6: function(arg0) {
|
|
||||||
const ret = arg0.ready;
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
__wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () {
|
__wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () {
|
||||||
const ret = module.require;
|
const ret = module.require;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -920,15 +1013,6 @@ function __wbg_get_imports() {
|
||||||
const ret = Reflect.set(arg0, arg1, arg2);
|
const ret = Reflect.set(arg0, arg1, arg2);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbg_set_algorithm_4884633ae550b091: function(arg0, arg1, arg2) {
|
|
||||||
arg0.algorithm = getStringFromWasm0(arg1, arg2);
|
|
||||||
},
|
|
||||||
__wbg_set_server_certificate_hashes_c6f10c7638672baf: function(arg0, arg1, arg2) {
|
|
||||||
arg0.serverCertificateHashes = getArrayJsValueViewFromWasm0(arg1, arg2);
|
|
||||||
},
|
|
||||||
__wbg_set_value_u8_array_38f1c892603c4916: function(arg0, arg1) {
|
|
||||||
arg0.value = arg1;
|
|
||||||
},
|
|
||||||
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
|
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
|
||||||
const ret = arg1.stack;
|
const ret = arg1.stack;
|
||||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
|
@ -952,10 +1036,6 @@ function __wbg_get_imports() {
|
||||||
const ret = typeof window === 'undefined' ? null : window;
|
const ret = typeof window === 'undefined' ? null : window;
|
||||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
},
|
},
|
||||||
__wbg_stringify_b54333f60f1e4dad: function() { return handleError(function (arg0) {
|
|
||||||
const ret = JSON.stringify(arg0);
|
|
||||||
return ret;
|
|
||||||
}, arguments); },
|
|
||||||
__wbg_subarray_3ed232c8a6baee09: function(arg0, arg1, arg2) {
|
__wbg_subarray_3ed232c8a6baee09: function(arg0, arg1, arg2) {
|
||||||
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -968,6 +1048,10 @@ function __wbg_get_imports() {
|
||||||
const ret = arg0.then(arg1);
|
const ret = arg0.then(arg1);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
__wbg_toString_34387d7c1df9ca1e: function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.toString(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
__wbg_versions_276b2795b1c6a219: function(arg0) {
|
__wbg_versions_276b2795b1c6a219: function(arg0) {
|
||||||
const ret = arg0.versions;
|
const ret = arg0.versions;
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -977,18 +1061,18 @@ function __wbg_get_imports() {
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 135, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 128, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WebTransportSendStream")], shim_idx: 64, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 65, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3c511b580d027299);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 64, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 63, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h99ead69e8d7f5bae);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000004: function(arg0) {
|
__wbindgen_cast_0000000000000004: function(arg0) {
|
||||||
|
|
@ -1027,6 +1111,14 @@ function __wbg_get_imports() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wasm_bindgen__convert__closures_____invoke__h99ead69e8d7f5bae(arg0, arg1) {
|
||||||
|
wasm.wasm_bindgen__convert__closures_____invoke__h99ead69e8d7f5bae(arg0, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasm_bindgen__convert__closures_____invoke__h3c511b580d027299(arg0, arg1, arg2) {
|
||||||
|
wasm.wasm_bindgen__convert__closures_____invoke__h3c511b580d027299(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg1, arg2) {
|
function wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg1, arg2) {
|
||||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg1, arg2);
|
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg1, arg2);
|
||||||
if (ret[1]) {
|
if (ret[1]) {
|
||||||
|
|
@ -1034,20 +1126,6 @@ function wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584(arg0, arg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f(arg0, arg1, arg2) {
|
|
||||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f(arg0, arg1, arg2);
|
|
||||||
if (ret[1]) {
|
|
||||||
throw takeFromExternrefTable0(ret[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2(arg0, arg1, arg2) {
|
|
||||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2(arg0, arg1, arg2);
|
|
||||||
if (ret[1]) {
|
|
||||||
throw takeFromExternrefTable0(ret[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424(arg0, arg1, arg2, arg3) {
|
function wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424(arg0, arg1, arg2, arg3) {
|
||||||
wasm.wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424(arg0, arg1, arg2, arg3);
|
wasm.wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424(arg0, arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
@ -1070,6 +1148,9 @@ const WasmKeyringFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
const WasmPublicKeyBundleFinalization = (typeof FinalizationRegistry === 'undefined')
|
const WasmPublicKeyBundleFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
? { register: () => {}, unregister: () => {} }
|
? { register: () => {}, unregister: () => {} }
|
||||||
: new FinalizationRegistry(ptr => wasm.__wbg_wasmpublickeybundle_free(ptr, 1));
|
: new FinalizationRegistry(ptr => wasm.__wbg_wasmpublickeybundle_free(ptr, 1));
|
||||||
|
const WasmSubscriptionRouterFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_wasmsubscriptionrouter_free(ptr, 1));
|
||||||
|
|
||||||
function addToExternrefTable0(obj) {
|
function addToExternrefTable0(obj) {
|
||||||
const idx = wasm.__externref_table_alloc();
|
const idx = wasm.__externref_table_alloc();
|
||||||
|
|
@ -1152,16 +1233,6 @@ function debugString(val) {
|
||||||
return className;
|
return className;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArrayJsValueViewFromWasm0(ptr, len) {
|
|
||||||
ptr = ptr >>> 0;
|
|
||||||
const mem = getDataViewMemory0();
|
|
||||||
const result = [];
|
|
||||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
|
||||||
result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true)));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArrayU8FromWasm0(ptr, len) {
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
ptr = ptr >>> 0;
|
ptr = ptr >>> 0;
|
||||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
|
|
||||||
Binary file not shown.
37
wasm/pkg/mtp_wasm_bg.wasm.d.ts
vendored
37
wasm/pkg/mtp_wasm_bg.wasm.d.ts
vendored
|
|
@ -1,6 +1,11 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const build_frame: (a: number, b: number, c: any, d: any) => [number, number, number, number];
|
||||||
|
export const build_ping_frame: (a: bigint, b: number, c: number, d: bigint, e: number, f: number) => [number, number, number, number];
|
||||||
|
export const format_frame: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const parse_auth_response: (a: number, b: number) => [number, number, number];
|
||||||
|
export const parse_frame: (a: number, b: number) => [number, number, number];
|
||||||
export const __wbg_wasmchacha20poly1305_free: (a: number, b: number) => void;
|
export const __wbg_wasmchacha20poly1305_free: (a: number, b: number) => void;
|
||||||
export const __wbg_wasmed25519signer_free: (a: number, b: number) => void;
|
export const __wbg_wasmed25519signer_free: (a: number, b: number) => void;
|
||||||
export const __wbg_wasmkeyring_free: (a: number, b: number) => void;
|
export const __wbg_wasmkeyring_free: (a: number, b: number) => void;
|
||||||
|
|
@ -26,32 +31,36 @@ export const wasmpublickeybundle_kem_public_key: (a: number) => [number, number]
|
||||||
export const wasmpublickeybundle_sig_cl_public_key: (a: number) => [number, number];
|
export const wasmpublickeybundle_sig_cl_public_key: (a: number) => [number, number];
|
||||||
export const wasmpublickeybundle_sig_pq_public_key: (a: number) => [number, number];
|
export const wasmpublickeybundle_sig_pq_public_key: (a: number) => [number, number];
|
||||||
export const wasmpublickeybundle_to_bytes: (a: number) => [number, number];
|
export const wasmpublickeybundle_to_bytes: (a: number) => [number, number];
|
||||||
export const build_demo_message: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
||||||
export const build_ping_frame: (a: bigint, b: number, c: number, d: bigint, e: number, f: number) => [number, number, number, number];
|
|
||||||
export const build_request_frame: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
||||||
export const format_frame: (a: number, b: number) => [number, number, number, number];
|
|
||||||
export const parse_auth_response: (a: number, b: number) => [number, number, number];
|
|
||||||
export const parse_response_frame: (a: number, b: number) => [number, number, number, number];
|
|
||||||
export const __wbg_connectionconfig_free: (a: number, b: number) => void;
|
|
||||||
export const __wbg_wasmclient_free: (a: number, b: number) => void;
|
export const __wbg_wasmclient_free: (a: number, b: number) => void;
|
||||||
export const connectionconfig_client_id: (a: number) => bigint;
|
|
||||||
export const connectionconfig_new: (a: number, b: number) => number;
|
|
||||||
export const connectionconfig_set_client_id: (a: number, b: bigint) => void;
|
|
||||||
export const connectionconfig_set_server_certificate_hashes: (a: number, b: number, c: number) => void;
|
|
||||||
export const connectionconfig_url: (a: number) => [number, number];
|
|
||||||
export const wasmclient_auth_connect: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => any;
|
export const wasmclient_auth_connect: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => any;
|
||||||
export const wasmclient_auth_register: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
export const wasmclient_auth_register: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
|
||||||
export const wasmclient_connect: (a: number, b: number) => any;
|
export const wasmclient_connect: (a: number, b: number) => any;
|
||||||
export const wasmclient_disconnect: (a: number) => void;
|
export const wasmclient_disconnect: (a: number) => void;
|
||||||
export const wasmclient_is_supported: () => number;
|
export const wasmclient_is_supported: () => number;
|
||||||
export const wasmclient_new: (a: any, b: any, c: any) => number;
|
export const wasmclient_new: (a: any, b: any, c: any) => number;
|
||||||
|
export const wasmclient_request: (a: number, b: number, c: number, d: number, e: number) => any;
|
||||||
export const wasmclient_send: (a: number, b: number, c: number) => any;
|
export const wasmclient_send: (a: number, b: number, c: number) => any;
|
||||||
|
export const wasmclient_start_protocol_pings: (a: number, b: number, c: bigint) => [number, number];
|
||||||
export const wasmclient_state: (a: number) => number;
|
export const wasmclient_state: (a: number) => number;
|
||||||
|
export const wasmclient_stop_protocol_pings: (a: number) => void;
|
||||||
|
export const wasmclient_subscribe: (a: number, b: number, c: number, d: any) => number;
|
||||||
|
export const wasmclient_unsubscribe: (a: number, b: number) => number;
|
||||||
|
export const __wbg_wasmsubscriptionrouter_free: (a: number, b: number) => void;
|
||||||
export const main: () => void;
|
export const main: () => void;
|
||||||
|
export const wasmsubscriptionrouter_dispatch: (a: number, b: number, c: number, d: any) => number;
|
||||||
|
export const wasmsubscriptionrouter_new: () => number;
|
||||||
|
export const wasmsubscriptionrouter_subscribe: (a: number, b: number, c: number, d: any) => void;
|
||||||
|
export const wasmsubscriptionrouter_unsubscribe: (a: number, b: number, c: number) => number;
|
||||||
|
export const __wbg_connectionconfig_free: (a: number, b: number) => void;
|
||||||
|
export const connectionconfig_client_id: (a: number) => bigint;
|
||||||
|
export const connectionconfig_new: (a: number, b: number) => number;
|
||||||
|
export const connectionconfig_set_client_id: (a: number, b: bigint) => void;
|
||||||
|
export const connectionconfig_set_server_certificate_hashes: (a: number, b: number, c: number) => void;
|
||||||
|
export const connectionconfig_url: (a: number) => [number, number];
|
||||||
export const wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number];
|
export const wasm_bindgen__convert__closures_____invoke__h6588d25cdde23584: (a: number, b: number, c: any) => [number, number];
|
||||||
export const wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f: (a: number, b: number, c: any) => [number, number];
|
|
||||||
export const wasm_bindgen__convert__closures_____invoke__h2faccefeed15993f_2: (a: number, b: number, c: any) => [number, number];
|
|
||||||
export const wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void;
|
export const wasm_bindgen__convert__closures_____invoke__h4bf2427f775cf424: (a: number, b: number, c: any, d: any) => void;
|
||||||
|
export const wasm_bindgen__convert__closures_____invoke__h3c511b580d027299: (a: number, b: number, c: any) => void;
|
||||||
|
export const wasm_bindgen__convert__closures_____invoke__h99ead69e8d7f5bae: (a: number, b: number) => void;
|
||||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
export const __wbindgen_exn_store: (a: number) => void;
|
export const __wbindgen_exn_store: (a: number) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use std::cell::Cell;
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use futures_channel::oneshot;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
||||||
|
|
@ -8,9 +11,116 @@ use mtp_type_map::CommunicationTypeId;
|
||||||
|
|
||||||
use mtp_crypto::SignatureScheme;
|
use mtp_crypto::SignatureScheme;
|
||||||
|
|
||||||
|
use crate::config::ConnectionConfig;
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
use crate::transport::WasmTransport;
|
use crate::transport::WasmTransport;
|
||||||
|
|
||||||
|
struct PendingRequest {
|
||||||
|
response_type: Option<String>,
|
||||||
|
sender: oneshot::Sender<Result<JsValue, JsValue>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PingTimer {
|
||||||
|
id: i32,
|
||||||
|
closure: Closure<dyn FnMut()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_property(frame: &JsValue, key: &str) -> Option<JsValue> {
|
||||||
|
js_sys::Reflect::get(frame, &JsValue::from_str(key))
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.is_null() && !value.is_undefined())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_id(frame: &JsValue) -> Option<u32> {
|
||||||
|
frame_property(frame, "id")
|
||||||
|
.and_then(|value| value.as_f64())
|
||||||
|
.map(|value| value as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_type(frame: &JsValue) -> Option<String> {
|
||||||
|
frame_property(frame, "type").and_then(|value| value.as_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn route_incoming_frame(
|
||||||
|
frame: &JsValue,
|
||||||
|
on_message: &js_sys::Function,
|
||||||
|
subscriptions: &Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
|
||||||
|
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
|
||||||
|
) {
|
||||||
|
let message_type = frame_type(frame);
|
||||||
|
|
||||||
|
if let Some(request_id) = frame_id(frame) {
|
||||||
|
let pending = pending_requests.borrow_mut().remove(&request_id);
|
||||||
|
if let Some(pending) = pending {
|
||||||
|
let type_matches = pending
|
||||||
|
.response_type
|
||||||
|
.as_ref()
|
||||||
|
.zip(message_type.as_ref())
|
||||||
|
.map(|(expected, actual)| expected == actual)
|
||||||
|
.unwrap_or(true);
|
||||||
|
if type_matches {
|
||||||
|
let _ = pending.sender.send(Ok(frame.clone()));
|
||||||
|
} else {
|
||||||
|
let actual = message_type.clone().unwrap_or_else(|| "unknown".into());
|
||||||
|
let _ = pending.sender.send(Err(js_error(&format!(
|
||||||
|
"unexpected response type: expected {}, got {}",
|
||||||
|
pending.response_type.unwrap_or_else(|| "unknown".into()),
|
||||||
|
actual
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(message_type) = message_type.as_ref() {
|
||||||
|
let matching_id = pending_requests
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find_map(|(id, pending)| match pending.response_type.as_ref() {
|
||||||
|
Some(response_type) if response_type == message_type => Some(*id),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
if let Some(id) = matching_id {
|
||||||
|
if let Some(pending) = pending_requests.borrow_mut().remove(&id) {
|
||||||
|
let _ = pending.sender.send(Ok(frame.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = on_message.call1(&JsValue::NULL, frame);
|
||||||
|
|
||||||
|
let Some(message_type) = message_type else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (_, (subscription_type, callback)) in subscriptions.borrow().iter() {
|
||||||
|
if subscription_type == &message_type {
|
||||||
|
let _ = callback.call1(&JsValue::NULL, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_ping_timer(ping_timer: &Rc<RefCell<Option<PingTimer>>>) {
|
||||||
|
let Some(timer) = ping_timer.borrow_mut().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Ok(clear_interval) =
|
||||||
|
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
|
||||||
|
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||||
|
{
|
||||||
|
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
|
||||||
|
}
|
||||||
|
drop(timer.closure);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_pending_requests(
|
||||||
|
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
|
||||||
|
message: &str,
|
||||||
|
) {
|
||||||
|
let pending = std::mem::take(&mut *pending_requests.borrow_mut());
|
||||||
|
for (_, pending) in pending {
|
||||||
|
let _ = pending.sender.send(Err(js_error(message)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn raw_frame_preview(bytes: &[u8]) -> String {
|
fn raw_frame_preview(bytes: &[u8]) -> String {
|
||||||
let shown = bytes.len().min(256);
|
let shown = bytes.len().min(256);
|
||||||
let mut preview = hex::encode(&bytes[..shown]);
|
let mut preview = hex::encode(&bytes[..shown]);
|
||||||
|
|
@ -139,45 +249,6 @@ pub enum ConnectionState {
|
||||||
Failed = 3,
|
Failed = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub struct ConnectionConfig {
|
|
||||||
url: String,
|
|
||||||
server_certificate_hashes: Option<Vec<String>>,
|
|
||||||
client_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
impl ConnectionConfig {
|
|
||||||
#[wasm_bindgen(constructor)]
|
|
||||||
pub fn new(url: String) -> Self {
|
|
||||||
Self {
|
|
||||||
url,
|
|
||||||
server_certificate_hashes: None,
|
|
||||||
client_id: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
|
||||||
pub fn url(&self) -> String {
|
|
||||||
self.url.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(setter)]
|
|
||||||
pub fn set_client_id(&mut self, id: u64) {
|
|
||||||
self.client_id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
|
||||||
pub fn client_id(&self) -> u64 {
|
|
||||||
self.client_id
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(setter)]
|
|
||||||
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
|
||||||
self.server_certificate_hashes = Some(hashes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct WasmClient {
|
pub struct WasmClient {
|
||||||
transport: Option<WasmTransport>,
|
transport: Option<WasmTransport>,
|
||||||
|
|
@ -185,6 +256,10 @@ pub struct WasmClient {
|
||||||
on_state_change: js_sys::Function,
|
on_state_change: js_sys::Function,
|
||||||
pub(crate) on_message: js_sys::Function,
|
pub(crate) on_message: js_sys::Function,
|
||||||
pub(crate) on_error: js_sys::Function,
|
pub(crate) on_error: js_sys::Function,
|
||||||
|
subscriptions: Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
|
||||||
|
next_subscription_id: Rc<Cell<u32>>,
|
||||||
|
pending_requests: Rc<RefCell<HashMap<u32, PendingRequest>>>,
|
||||||
|
ping_timer: Rc<RefCell<Option<PingTimer>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -201,6 +276,10 @@ impl WasmClient {
|
||||||
on_state_change: on_state_change.clone(),
|
on_state_change: on_state_change.clone(),
|
||||||
on_message: on_message.clone(),
|
on_message: on_message.clone(),
|
||||||
on_error: on_error.clone(),
|
on_error: on_error.clone(),
|
||||||
|
subscriptions: Rc::new(RefCell::new(HashMap::new())),
|
||||||
|
next_subscription_id: Rc::new(Cell::new(1)),
|
||||||
|
pending_requests: Rc::new(RefCell::new(HashMap::new())),
|
||||||
|
ping_timer: Rc::new(RefCell::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,12 +534,134 @@ impl WasmClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn request(
|
||||||
|
&self,
|
||||||
|
frame: Vec<u8>,
|
||||||
|
response_type: Option<String>,
|
||||||
|
) -> Result<JsValue, JsValue> {
|
||||||
|
let request = CommunicationValue::from_bytes(&frame)
|
||||||
|
.map_err(|e| js_error(&format!("parse request: {}", e)))?;
|
||||||
|
let request_id = request.get_id();
|
||||||
|
if request_id == 0 {
|
||||||
|
return Err(js_error("request frame must have a non-zero id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(transport) = self.transport.clone() else {
|
||||||
|
return Err(js_error("not connected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let (sender, receiver) = oneshot::channel();
|
||||||
|
self.pending_requests.borrow_mut().insert(
|
||||||
|
request_id,
|
||||||
|
PendingRequest {
|
||||||
|
response_type,
|
||||||
|
sender,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(error) = transport.send_frame(&frame).await {
|
||||||
|
self.pending_requests.borrow_mut().remove(&request_id);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
match receiver.await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => Err(js_error("request cancelled")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn subscribe(&self, message_type: String, callback: js_sys::Function) -> u32 {
|
||||||
|
let id = self.next_subscription_id.get();
|
||||||
|
self.next_subscription_id.set(id.wrapping_add(1).max(1));
|
||||||
|
self.subscriptions
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(id, (message_type, callback));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn unsubscribe(&self, id: u32) -> bool {
|
||||||
|
self.subscriptions.borrow_mut().remove(&id).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn start_protocol_pings(&self, interval_ms: u32, client_id: u64) -> Result<(), JsValue> {
|
||||||
|
self.stop_protocol_pings();
|
||||||
|
let Some(transport) = self.transport.clone() else {
|
||||||
|
return Err(js_error("not connected"));
|
||||||
|
};
|
||||||
|
let interval_ms = interval_ms.max(1_000) as i32;
|
||||||
|
let on_error = self.on_error.clone();
|
||||||
|
let closure = Closure::wrap(Box::new(move || {
|
||||||
|
let transport = transport.clone();
|
||||||
|
let on_error = on_error.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let timestamp = js_sys::Date::now() as u64;
|
||||||
|
let frame = CommunicationValue::new(CommunicationType::Ping)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Description,
|
||||||
|
DataValue::Str("protocol ping".into()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Timestamp,
|
||||||
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
|
)
|
||||||
|
.with_sender(client_id)
|
||||||
|
.to_bytes()
|
||||||
|
.map_err(|e| js_error(&format!("encode ping failed: {}", e)));
|
||||||
|
match frame {
|
||||||
|
Ok(frame) => {
|
||||||
|
if let Err(error) = transport.send_frame(&frame).await {
|
||||||
|
let _ = on_error.call1(&JsValue::NULL, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let _ = on_error.call1(&JsValue::NULL, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
let set_interval =
|
||||||
|
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setInterval"))?
|
||||||
|
.dyn_into::<js_sys::Function>()?;
|
||||||
|
let id = set_interval
|
||||||
|
.call2(
|
||||||
|
&JsValue::NULL,
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
&JsValue::from_f64(interval_ms as f64),
|
||||||
|
)?
|
||||||
|
.as_f64()
|
||||||
|
.ok_or_else(|| js_error("setInterval did not return an id"))? as i32;
|
||||||
|
*self.ping_timer.borrow_mut() = Some(PingTimer { id, closure });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn stop_protocol_pings(&self) {
|
||||||
|
let Some(timer) = self.ping_timer.borrow_mut().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Ok(clear_interval) =
|
||||||
|
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
|
||||||
|
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||||
|
{
|
||||||
|
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
|
||||||
|
}
|
||||||
|
drop(timer.closure);
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn disconnect(&mut self) {
|
pub fn disconnect(&mut self) {
|
||||||
|
self.stop_protocol_pings();
|
||||||
if let Some(t) = &self.transport {
|
if let Some(t) = &self.transport {
|
||||||
t.close();
|
t.close();
|
||||||
}
|
}
|
||||||
self.transport = None;
|
self.transport = None;
|
||||||
|
self.subscriptions.borrow_mut().clear();
|
||||||
|
self.reject_pending_requests("disconnected");
|
||||||
self.set_state(ConnectionState::Disconnected);
|
self.set_state(ConnectionState::Disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,12 +680,34 @@ impl WasmClient {
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let on_msg = self.on_message.clone();
|
let on_msg = self.on_message.clone();
|
||||||
let on_err = self.on_error.clone();
|
let on_err = self.on_error.clone();
|
||||||
|
let subscriptions = self.subscriptions.clone();
|
||||||
|
let pending_requests = self.pending_requests.clone();
|
||||||
|
let loop_pending_requests = pending_requests.clone();
|
||||||
|
let ping_timer = self.ping_timer.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
loop_transport.receive_loop(on_msg, on_err).await;
|
let route_frame = Closure::wrap(Box::new(move |frame: JsValue| {
|
||||||
|
route_incoming_frame(&frame, &on_msg, &subscriptions, &loop_pending_requests);
|
||||||
|
}) as Box<dyn FnMut(JsValue)>);
|
||||||
|
loop_transport
|
||||||
|
.receive_loop(
|
||||||
|
route_frame
|
||||||
|
.as_ref()
|
||||||
|
.unchecked_ref::<js_sys::Function>()
|
||||||
|
.clone(),
|
||||||
|
on_err.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
drop(route_frame);
|
||||||
state.set(ConnectionState::Disconnected);
|
state.set(ConnectionState::Disconnected);
|
||||||
|
stop_ping_timer(&ping_timer);
|
||||||
|
reject_pending_requests(&pending_requests, "disconnected");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reject_pending_requests(&self, message: &str) {
|
||||||
|
reject_pending_requests(&self.pending_requests, message);
|
||||||
|
}
|
||||||
|
|
||||||
async fn read_verified_challenge(
|
async fn read_verified_challenge(
|
||||||
&self,
|
&self,
|
||||||
transport: &WasmTransport,
|
transport: &WasmTransport,
|
||||||
|
|
|
||||||
40
wasm/src/config.rs
Normal file
40
wasm/src/config.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct ConnectionConfig {
|
||||||
|
pub(crate) url: String,
|
||||||
|
pub(crate) server_certificate_hashes: Option<Vec<String>>,
|
||||||
|
pub(crate) client_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl ConnectionConfig {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(url: String) -> Self {
|
||||||
|
Self {
|
||||||
|
url,
|
||||||
|
server_certificate_hashes: None,
|
||||||
|
client_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter)]
|
||||||
|
pub fn url(&self) -> String {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(setter)]
|
||||||
|
pub fn set_client_id(&mut self, id: u64) {
|
||||||
|
self.client_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter)]
|
||||||
|
pub fn client_id(&self) -> u64 {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(setter)]
|
||||||
|
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
||||||
|
self.server_certificate_hashes = Some(hashes);
|
||||||
|
}
|
||||||
|
}
|
||||||
479
wasm/src/frame.rs
Normal file
479
wasm/src/frame.rs
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
use wasm_bindgen::{JsCast, prelude::*};
|
||||||
|
|
||||||
|
use mtp_codec::{
|
||||||
|
CommunicationType, CommunicationValue, DataType, DataValue, communication_type_name,
|
||||||
|
data_type_name,
|
||||||
|
};
|
||||||
|
use mtp_type_map::TypeMap;
|
||||||
|
|
||||||
|
use crate::error::js_error;
|
||||||
|
|
||||||
|
#[wasm_bindgen(typescript_custom_section)]
|
||||||
|
const PARSED_FRAME_TS: &'static str = r#"
|
||||||
|
export interface ParsedFrame {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
sender?: bigint;
|
||||||
|
receiver?: bigint;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
raw: Uint8Array;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn set_prop(obj: &js_sys::Object, key: &str, value: &JsValue) -> Result<(), JsValue> {
|
||||||
|
js_sys::Reflect::set(obj, &JsValue::from_str(key), value).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integer_value(value: &str) -> JsValue {
|
||||||
|
if let Ok(number) = value.parse::<f64>() {
|
||||||
|
if number.fract() == 0.0 && number.abs() <= 9_007_199_254_740_991.0 {
|
||||||
|
return JsValue::from_f64(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsValue::from_str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
|
||||||
|
match value {
|
||||||
|
DataValue::BoolTrue => Ok(JsValue::TRUE),
|
||||||
|
DataValue::BoolFalse => Ok(JsValue::FALSE),
|
||||||
|
DataValue::Bool(v) => Ok(JsValue::from_bool(*v)),
|
||||||
|
DataValue::SignedNumber(n) => Ok(integer_value(&n.to_string())),
|
||||||
|
DataValue::UnsignedNumber(n) => Ok(integer_value(&n.to_string())),
|
||||||
|
DataValue::Float(exp, mant) => {
|
||||||
|
Ok(JsValue::from_f64((*mant as f64) * 10f64.powi(*exp as i32)))
|
||||||
|
}
|
||||||
|
DataValue::Str(s) => Ok(JsValue::from_str(s)),
|
||||||
|
DataValue::Bytes(bytes) => Ok(js_sys::Uint8Array::from(&bytes[..]).into()),
|
||||||
|
DataValue::Array(values) => {
|
||||||
|
let arr = js_sys::Array::new();
|
||||||
|
for value in values {
|
||||||
|
arr.push(&data_value_to_js(value)?);
|
||||||
|
}
|
||||||
|
Ok(arr.into())
|
||||||
|
}
|
||||||
|
DataValue::Container(entries) => {
|
||||||
|
let obj = js_sys::Object::new();
|
||||||
|
for (key, value) in entries {
|
||||||
|
let name = data_type_name(key.0)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| key.0.to_string());
|
||||||
|
set_prop(&obj, &name, &data_value_to_js(value)?)?;
|
||||||
|
}
|
||||||
|
Ok(obj.into())
|
||||||
|
}
|
||||||
|
DataValue::EncryptedContainer(bytes)
|
||||||
|
| DataValue::SignedContainer(bytes)
|
||||||
|
| DataValue::SignedEncryptedContainer(bytes) => {
|
||||||
|
Ok(js_sys::Uint8Array::from(&bytes[..]).into())
|
||||||
|
}
|
||||||
|
DataValue::Null => Ok(JsValue::NULL),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
|
||||||
|
if value.is_null() || value.is_undefined() {
|
||||||
|
return Ok(DataValue::Null);
|
||||||
|
}
|
||||||
|
if let Some(v) = value.as_bool() {
|
||||||
|
return Ok(DataValue::Bool(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = value.as_string() {
|
||||||
|
return Ok(DataValue::Str(v));
|
||||||
|
}
|
||||||
|
if js_sys::Uint8Array::instanceof(value) {
|
||||||
|
return Ok(DataValue::Bytes(js_sys::Uint8Array::new(value).to_vec()));
|
||||||
|
}
|
||||||
|
if js_sys::Array::is_array(value) {
|
||||||
|
let array = js_sys::Array::from(value);
|
||||||
|
let mut values = Vec::with_capacity(array.length() as usize);
|
||||||
|
for item in array.iter() {
|
||||||
|
values.push(js_to_data_value(&item)?);
|
||||||
|
}
|
||||||
|
return Ok(DataValue::Array(values));
|
||||||
|
}
|
||||||
|
if let Some(v) = value.as_f64() {
|
||||||
|
if v.fract() == 0.0 {
|
||||||
|
if v >= 0.0 {
|
||||||
|
return Ok(DataValue::UnsignedNumber(v as u128));
|
||||||
|
}
|
||||||
|
return Ok(DataValue::SignedNumber(v as i128));
|
||||||
|
}
|
||||||
|
let mantissa = (v * 1_000_000.0).round() as u32;
|
||||||
|
return Ok(DataValue::Float(246, mantissa));
|
||||||
|
}
|
||||||
|
|
||||||
|
let type_name = value.js_typeof().as_string().unwrap_or_default();
|
||||||
|
if type_name == "bigint" {
|
||||||
|
let bigint = value.clone().unchecked_into::<js_sys::BigInt>();
|
||||||
|
let as_string = bigint
|
||||||
|
.to_string(10)?
|
||||||
|
.as_string()
|
||||||
|
.ok_or_else(|| js_error("failed to stringify bigint"))?;
|
||||||
|
if let Some(unsigned) = as_string.strip_prefix('-') {
|
||||||
|
let n = unsigned
|
||||||
|
.parse::<i128>()
|
||||||
|
.map_err(|_| js_error("bigint out of range"))?;
|
||||||
|
return Ok(DataValue::SignedNumber(-n));
|
||||||
|
}
|
||||||
|
let n = as_string
|
||||||
|
.parse::<u128>()
|
||||||
|
.map_err(|_| js_error("bigint out of range"))?;
|
||||||
|
return Ok(DataValue::UnsignedNumber(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.is_object() {
|
||||||
|
let object = js_sys::Object::from(value.clone());
|
||||||
|
let keys = js_sys::Object::keys(&object);
|
||||||
|
let mut entries = Vec::with_capacity(keys.length() as usize);
|
||||||
|
for key in keys.iter() {
|
||||||
|
let key = key
|
||||||
|
.as_string()
|
||||||
|
.ok_or_else(|| js_error("object key must be a string"))?;
|
||||||
|
let data_type = DataType::from_name(&key)
|
||||||
|
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
|
||||||
|
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
|
||||||
|
entries.push((
|
||||||
|
data_type.to_id(&TypeMap::latest()),
|
||||||
|
js_to_data_value(&value)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(DataValue::Container(entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(js_error("unsupported data value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_u32(options: &JsValue, key: &str) -> Result<Option<u32>, JsValue> {
|
||||||
|
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
|
||||||
|
if value.is_null() || value.is_undefined() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let Some(n) = value.as_f64() else {
|
||||||
|
return Err(js_error(&format!("{key} must be a number")));
|
||||||
|
};
|
||||||
|
Ok(Some(n as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
|
||||||
|
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
|
||||||
|
if value.is_null() || value.is_undefined() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if let Some(n) = value.as_f64() {
|
||||||
|
return Ok(Some(n as u64));
|
||||||
|
}
|
||||||
|
let type_name = value.js_typeof().as_string().unwrap_or_default();
|
||||||
|
if type_name == "bigint" {
|
||||||
|
let bigint = value.clone().unchecked_into::<js_sys::BigInt>();
|
||||||
|
let as_string = bigint
|
||||||
|
.to_string(10)?
|
||||||
|
.as_string()
|
||||||
|
.ok_or_else(|| js_error("failed to stringify bigint"))?;
|
||||||
|
return as_string
|
||||||
|
.parse::<u64>()
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| js_error(&format!("{key} out of range")));
|
||||||
|
}
|
||||||
|
Err(js_error(&format!("{key} must be a number or bigint")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
|
||||||
|
let comm = CommunicationValue::from_bytes(frame)
|
||||||
|
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||||
|
let obj = js_sys::Object::new();
|
||||||
|
let data = js_sys::Object::new();
|
||||||
|
|
||||||
|
if comm.get_id() != 0 {
|
||||||
|
set_prop(&obj, "id", &JsValue::from_f64(comm.get_id() as f64))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame_type = communication_type_name(comm.get_type().0)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| comm.get_type().0.to_string());
|
||||||
|
set_prop(&obj, "type", &JsValue::from_str(&frame_type))?;
|
||||||
|
|
||||||
|
if comm.get_sender() != 0 {
|
||||||
|
set_prop(
|
||||||
|
&obj,
|
||||||
|
"sender",
|
||||||
|
&JsValue::bigint_from_str(&comm.get_sender().to_string()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if comm.get_receiver() != 0 {
|
||||||
|
set_prop(
|
||||||
|
&obj,
|
||||||
|
"receiver",
|
||||||
|
&JsValue::bigint_from_str(&comm.get_receiver().to_string()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in comm.data() {
|
||||||
|
let name = data_type_name(key.0)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| key.0.to_string());
|
||||||
|
set_prop(&data, &name, &data_value_to_js(value)?)?;
|
||||||
|
}
|
||||||
|
set_prop(&obj, "data", &data.into())?;
|
||||||
|
set_prop(&obj, "raw", &js_sys::Uint8Array::from(frame).into())?;
|
||||||
|
|
||||||
|
Ok(obj.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a protocol-level Ping frame with description, timestamp, and optional data.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn build_ping_frame(
|
||||||
|
client_id: u64,
|
||||||
|
description: &str,
|
||||||
|
timestamp: u64,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let mut msg = CommunicationValue::new(CommunicationType::Ping)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Description,
|
||||||
|
DataValue::Str(description.to_string()),
|
||||||
|
)
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::Timestamp,
|
||||||
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
|
)
|
||||||
|
.with_sender(client_id);
|
||||||
|
|
||||||
|
if !data.is_empty() {
|
||||||
|
msg = msg.add_typed_default(DataType::Id, DataValue::Bytes(data.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.to_bytes()
|
||||||
|
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an auth response frame into a JS object.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
|
||||||
|
let comm = CommunicationValue::from_bytes(response)
|
||||||
|
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||||
|
|
||||||
|
let connected = matches!(
|
||||||
|
comm.get_data(DataType::Connected.to_id(&TypeMap::latest())),
|
||||||
|
DataValue::BoolTrue
|
||||||
|
);
|
||||||
|
|
||||||
|
let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) {
|
||||||
|
DataValue::UnsignedNumber(n) => Some(*n),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) {
|
||||||
|
DataValue::UnsignedNumber(n) => Some(*n as u64),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) {
|
||||||
|
DataValue::UnsignedNumber(n) => Some(*n),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) {
|
||||||
|
DataValue::Bytes(b) => Some(b.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let obj = js_sys::Object::new();
|
||||||
|
js_sys::Reflect::set(&obj, &"connected".into(), &JsValue::from(connected)).ok();
|
||||||
|
if let Some(n) = client_nonce {
|
||||||
|
let arr = js_sys::Uint8Array::from(&n.to_be_bytes()[..]);
|
||||||
|
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
|
||||||
|
}
|
||||||
|
if let Some(id) = assigned_id {
|
||||||
|
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
|
||||||
|
}
|
||||||
|
if let Some(ts) = timestamp {
|
||||||
|
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
|
||||||
|
}
|
||||||
|
if let Some(sig) = signature {
|
||||||
|
let arr = js_sys::Uint8Array::from(&sig[..]);
|
||||||
|
js_sys::Reflect::set(&obj, &"signature".into(), &arr).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(obj.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse any MTP frame into the human-readable CommunicationValue display form.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
|
||||||
|
let comm = CommunicationValue::from_bytes(frame)
|
||||||
|
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
||||||
|
Ok(comm.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse any MTP frame into structured JavaScript data.
|
||||||
|
#[wasm_bindgen(unchecked_return_type = "ParsedFrame")]
|
||||||
|
pub fn parse_frame(frame: &[u8]) -> Result<JsValue, JsValue> {
|
||||||
|
parse_frame_value(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a typed MTP frame using generated communication/data type names.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn build_frame(
|
||||||
|
message_type: &str,
|
||||||
|
data: JsValue,
|
||||||
|
options: JsValue,
|
||||||
|
) -> Result<Vec<u8>, JsValue> {
|
||||||
|
let comm_type = CommunicationType::from_name(message_type)
|
||||||
|
.ok_or_else(|| js_error(&format!("unknown communication type: {message_type}")))?;
|
||||||
|
let mut msg = CommunicationValue::new(comm_type);
|
||||||
|
|
||||||
|
if !options.is_null() && !options.is_undefined() {
|
||||||
|
if let Some(id) = option_u32(&options, "id")? {
|
||||||
|
msg = msg.with_id(id);
|
||||||
|
}
|
||||||
|
if let Some(sender) = option_u64(&options, "sender")? {
|
||||||
|
msg = msg.with_sender(sender);
|
||||||
|
}
|
||||||
|
if let Some(receiver) = option_u64(&options, "receiver")? {
|
||||||
|
msg = msg.with_receiver(receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.is_object() && !js_sys::Uint8Array::instanceof(&data) && !js_sys::Array::is_array(&data)
|
||||||
|
{
|
||||||
|
let object = js_sys::Object::from(data);
|
||||||
|
let keys = js_sys::Object::keys(&object);
|
||||||
|
for key in keys.iter() {
|
||||||
|
let key = key
|
||||||
|
.as_string()
|
||||||
|
.ok_or_else(|| js_error("object key must be a string"))?;
|
||||||
|
let data_type = DataType::from_name(&key)
|
||||||
|
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
|
||||||
|
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
|
||||||
|
msg = msg.add_data(
|
||||||
|
data_type.to_id(&TypeMap::latest()),
|
||||||
|
js_to_data_value(&value)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if !data.is_null() && !data.is_undefined() {
|
||||||
|
return Err(js_error(
|
||||||
|
"frame data must be an object keyed by MTP data type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.to_bytes()
|
||||||
|
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn build_ping_frame_roundtrip() {
|
||||||
|
let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed");
|
||||||
|
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
||||||
|
let tm = TypeMap::latest();
|
||||||
|
|
||||||
|
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
||||||
|
assert_eq!(cv.get_sender(), 42);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataType::Description.to_id(&tm)),
|
||||||
|
&DataValue::Str("test-ping".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataType::Timestamp.to_id(&tm)),
|
||||||
|
&DataValue::UnsignedNumber(1234567890)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn build_ping_frame_with_data() {
|
||||||
|
let payload = b"attachment-data";
|
||||||
|
let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed");
|
||||||
|
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
||||||
|
let tm = TypeMap::latest();
|
||||||
|
|
||||||
|
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
||||||
|
assert_eq!(cv.get_sender(), 99);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataType::Description.to_id(&tm)),
|
||||||
|
&DataValue::Str("with-data".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataType::Timestamp.to_id(&tm)),
|
||||||
|
&DataValue::UnsignedNumber(555)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cv.get_data(DataType::Id.to_id(&tm)),
|
||||||
|
&DataValue::Bytes(payload.to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn build_ping_frame_client_id_zero() {
|
||||||
|
let bytes = build_ping_frame(0, "zero-id", 0, &[]).expect("encode failed");
|
||||||
|
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
||||||
|
assert_eq!(cv.get_sender(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn parse_auth_response_success() {
|
||||||
|
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
||||||
|
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
||||||
|
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
|
||||||
|
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
|
||||||
|
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
|
||||||
|
.to_bytes()
|
||||||
|
.expect("encode failed");
|
||||||
|
|
||||||
|
let result = parse_auth_response(&resp).expect("parse failed");
|
||||||
|
|
||||||
|
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.as_bool());
|
||||||
|
assert_eq!(connected, Some(true));
|
||||||
|
|
||||||
|
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.as_f64());
|
||||||
|
assert_eq!(id, Some(42.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn parse_auth_response_rejected() {
|
||||||
|
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
||||||
|
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
||||||
|
.to_bytes()
|
||||||
|
.expect("encode failed");
|
||||||
|
|
||||||
|
let result = parse_auth_response(&resp).expect("parse failed");
|
||||||
|
|
||||||
|
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.as_bool());
|
||||||
|
assert_eq!(connected, Some(false));
|
||||||
|
|
||||||
|
let has_id = js_sys::Reflect::has(&result, &"assignedId".into()).unwrap_or(false);
|
||||||
|
assert!(!has_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn parse_auth_response_with_signature() {
|
||||||
|
let sig_bytes = vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe];
|
||||||
|
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
||||||
|
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
||||||
|
.add_typed_default(DataType::Signature, DataValue::Bytes(sig_bytes.clone()))
|
||||||
|
.to_bytes()
|
||||||
|
.expect("encode failed");
|
||||||
|
|
||||||
|
let result = parse_auth_response(&resp).expect("parse failed");
|
||||||
|
|
||||||
|
let has_sig = js_sys::Reflect::has(&result, &"signature".into()).unwrap_or(false);
|
||||||
|
assert!(has_sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn parse_auth_response_invalid_frame() {
|
||||||
|
let result = parse_auth_response(b"garbage-data");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod config;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod message;
|
pub mod frame;
|
||||||
|
pub mod logging;
|
||||||
|
pub mod subscription;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
|
@ -11,5 +14,4 @@ use wasm_bindgen::prelude::*;
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
web_sys::console::log_1(&"mtp-wasm: module loaded".into());
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
wasm/src/logging.rs
Normal file
10
wasm/src/logging.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// Log severity used by the public SDK when translating raw WASM events.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WasmLogHint {
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Error = 2,
|
||||||
|
}
|
||||||
|
|
@ -1,411 +0,0 @@
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
|
|
||||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
|
|
||||||
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm};
|
|
||||||
use mtp_type_map::{TypeMap, communication_type_name};
|
|
||||||
|
|
||||||
use crate::error::js_error;
|
|
||||||
|
|
||||||
/// Build a simple Ping frame with description, timestamp, and optional data.
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn build_ping_frame(
|
|
||||||
client_id: u64,
|
|
||||||
description: &str,
|
|
||||||
timestamp: u64,
|
|
||||||
data: &[u8],
|
|
||||||
) -> Result<Vec<u8>, JsValue> {
|
|
||||||
let mut msg = CommunicationValue::new(CommunicationType::Ping)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::Description,
|
|
||||||
DataValue::Str(description.to_string()),
|
|
||||||
)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::Timestamp,
|
|
||||||
DataValue::UnsignedNumber(timestamp as u128),
|
|
||||||
)
|
|
||||||
.with_sender(client_id);
|
|
||||||
|
|
||||||
if !data.is_empty() {
|
|
||||||
msg = msg.add_typed_default(DataType::Id, DataValue::Bytes(data.to_vec()));
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.to_bytes()
|
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a demo Ping frame with encrypted and signed containers
|
|
||||||
/// (mirrors the Rust client example but uses only reserved data types).
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn build_demo_message(
|
|
||||||
client_id: u64,
|
|
||||||
keyring_bytes: &[u8],
|
|
||||||
host_bundle_bytes: &[u8],
|
|
||||||
) -> Result<Vec<u8>, JsValue> {
|
|
||||||
let keyring = Keyring::from_bytes(keyring_bytes)
|
|
||||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
|
||||||
|
|
||||||
// Encrypt to the server's KEM public key; the server decrypts with its keyring.
|
|
||||||
// (The client keyring only needs the Ed25519 signing key for this demo.)
|
|
||||||
let recipient = PublicKeyBundle::from_bytes(host_bundle_bytes)
|
|
||||||
.map_err(|e| js_error(&format!("invalid host bundle: {}", e)))?;
|
|
||||||
let enc_type = EncryptionType::MlKemChaCha20Poly1305;
|
|
||||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
|
||||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
|
||||||
|
|
||||||
// Encrypted container
|
|
||||||
let inner_enc = DataValue::Container(vec![
|
|
||||||
(
|
|
||||||
DataType::Version.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::Str("secret inner data".into()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
DataType::Id.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::UnsignedNumber(42),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
let mut dv_enc = inner_enc;
|
|
||||||
dv_enc
|
|
||||||
.encrypt_container(enc_type, &recipient, b"demo-aad")
|
|
||||||
.ok_or_else(|| js_error("encryption failed"))?;
|
|
||||||
|
|
||||||
// Signed container
|
|
||||||
let inner_sig = DataValue::Container(vec![
|
|
||||||
(
|
|
||||||
DataType::Version.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::Str("signed by client".into()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
DataType::Id.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::UnsignedNumber(99),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
let mut dv_sig = inner_sig;
|
|
||||||
dv_sig
|
|
||||||
.sign_container(SigAlgorithm::ED25519, &signer)
|
|
||||||
.ok_or_else(|| js_error("signing failed"))?;
|
|
||||||
|
|
||||||
// Signed + encrypted container
|
|
||||||
let inner_sec = DataValue::Container(vec![
|
|
||||||
(
|
|
||||||
DataType::Version.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::Str("signed+encrypted payload".into()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
DataType::Id.to_id(&TypeMap::latest()),
|
|
||||||
DataValue::UnsignedNumber(7),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
let mut dv_sec = inner_sec;
|
|
||||||
dv_sec
|
|
||||||
.sign_and_encrypt_container(
|
|
||||||
SigAlgorithm::ED25519,
|
|
||||||
&signer,
|
|
||||||
enc_type,
|
|
||||||
&recipient,
|
|
||||||
b"demo-aad",
|
|
||||||
)
|
|
||||||
.ok_or_else(|| js_error("sign+encrypt failed"))?;
|
|
||||||
|
|
||||||
let timestamp = js_sys::Date::now() as u64;
|
|
||||||
|
|
||||||
let msg = CommunicationValue::new(CommunicationType::Ping)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::Description,
|
|
||||||
DataValue::Str("MTP WASM Demo".into()),
|
|
||||||
)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::Timestamp,
|
|
||||||
DataValue::UnsignedNumber(timestamp as u128),
|
|
||||||
)
|
|
||||||
.add_typed_default(DataType::Version, DataValue::Str("demo-wasm".into()))
|
|
||||||
.with_sender(client_id);
|
|
||||||
|
|
||||||
msg.to_bytes()
|
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse an auth response frame into a JS object.
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
|
|
||||||
let comm = CommunicationValue::from_bytes(response)
|
|
||||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
|
||||||
|
|
||||||
let connected = matches!(
|
|
||||||
comm.get_data(DataType::Connected.to_id(&TypeMap::latest())),
|
|
||||||
DataValue::BoolTrue
|
|
||||||
);
|
|
||||||
|
|
||||||
let client_nonce = match comm.get_data(DataType::ClientNonce.to_id(&TypeMap::latest())) {
|
|
||||||
DataValue::UnsignedNumber(n) => Some(*n),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let assigned_id = match comm.get_data(DataType::Id.to_id(&TypeMap::latest())) {
|
|
||||||
DataValue::UnsignedNumber(n) => Some(*n as u64),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let timestamp = match comm.get_data(DataType::Timestamp.to_id(&TypeMap::latest())) {
|
|
||||||
DataValue::UnsignedNumber(n) => Some(*n),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let signature = match comm.get_data(DataType::Signature.to_id(&TypeMap::latest())) {
|
|
||||||
DataValue::Bytes(b) => Some(b.clone()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let obj = js_sys::Object::new();
|
|
||||||
js_sys::Reflect::set(&obj, &"connected".into(), &JsValue::from(connected)).ok();
|
|
||||||
if let Some(n) = client_nonce {
|
|
||||||
let arr = js_sys::Uint8Array::from(&n.to_be_bytes()[..]);
|
|
||||||
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
|
|
||||||
}
|
|
||||||
if let Some(id) = assigned_id {
|
|
||||||
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
|
|
||||||
}
|
|
||||||
if let Some(ts) = timestamp {
|
|
||||||
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
|
|
||||||
}
|
|
||||||
if let Some(sig) = signature {
|
|
||||||
let arr = js_sys::Uint8Array::from(&sig[..]);
|
|
||||||
js_sys::Reflect::set(&obj, &"signature".into(), &arr).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(obj.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a request frame with the given communication type name, request ID, and JSON data.
|
|
||||||
///
|
|
||||||
/// - `comm_type`: communication type name (e.g. "get_model", "rank_models") or PascalCase
|
|
||||||
/// - `id`: request ID for response correlation
|
|
||||||
/// - `json_data`: JSON-stringified request payload
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn build_request_frame(comm_type: &str, id: u32, json_data: &str) -> Result<Vec<u8>, JsValue> {
|
|
||||||
let comm_type_enum = CommunicationType::from_name(comm_type)
|
|
||||||
.or_else(|| {
|
|
||||||
let pascal = comm_type
|
|
||||||
.split('_')
|
|
||||||
.map(|s| {
|
|
||||||
let mut c = s.chars();
|
|
||||||
match c.next() {
|
|
||||||
None => String::new(),
|
|
||||||
Some(f) => f.to_uppercase().to_string() + c.as_str(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<String>();
|
|
||||||
CommunicationType::from_name(&pascal)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| js_error(&format!("unknown communication type: {}", comm_type)))?;
|
|
||||||
|
|
||||||
let frame = CommunicationValue::new(comm_type_enum)
|
|
||||||
.with_id(id)
|
|
||||||
.add_data(DataTypeId(32), DataValue::Str(json_data.to_string()))
|
|
||||||
.to_bytes()
|
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a response frame into a JSON string containing `_id`, `_type`, and data fields.
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
|
|
||||||
let comm = CommunicationValue::from_bytes(frame)
|
|
||||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
|
||||||
|
|
||||||
let obj = js_sys::Object::new();
|
|
||||||
|
|
||||||
let _ = js_sys::Reflect::set(
|
|
||||||
&obj,
|
|
||||||
&JsValue::from_str("_id"),
|
|
||||||
&JsValue::from(comm.get_id()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let type_name = communication_type_name(comm.get_type().0).unwrap_or("Unknown");
|
|
||||||
let _ = js_sys::Reflect::set(
|
|
||||||
&obj,
|
|
||||||
&JsValue::from_str("_type"),
|
|
||||||
&JsValue::from_str(&type_name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let DataValue::Str(s) = comm.get_data(DataTypeId(32)) {
|
|
||||||
if let Ok(parsed) = js_sys::JSON::parse(s) {
|
|
||||||
let parsed_obj: &js_sys::Object = parsed.unchecked_ref();
|
|
||||||
let entries = js_sys::Object::entries(parsed_obj);
|
|
||||||
let len = entries.length();
|
|
||||||
for i in 0..len {
|
|
||||||
let entry = js_sys::Array::get(&entries, i);
|
|
||||||
if let Some(entry_arr) = entry.dyn_ref::<js_sys::Array>() {
|
|
||||||
if let Some(key) = entry_arr.get(0).as_string() {
|
|
||||||
let val = entry_arr.get(1);
|
|
||||||
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str(&key), &val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let stringified =
|
|
||||||
js_sys::JSON::stringify(&obj).map_err(|_| js_error("JSON stringify failed"))?;
|
|
||||||
stringified
|
|
||||||
.as_string()
|
|
||||||
.ok_or_else(|| js_error("JSON stringify result not a string"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse any MTP frame into the human-readable CommunicationValue display form.
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
|
|
||||||
let comm = CommunicationValue::from_bytes(frame)
|
|
||||||
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
|
|
||||||
Ok(comm.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use wasm_bindgen_test::*;
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn build_ping_frame_roundtrip() {
|
|
||||||
let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]).expect("encode failed");
|
|
||||||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
|
||||||
let tm = TypeMap::latest();
|
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
|
||||||
assert_eq!(cv.get_sender(), 42);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Description.to_id(&tm)),
|
|
||||||
&DataValue::Str("test-ping".into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Timestamp.to_id(&tm)),
|
|
||||||
&DataValue::UnsignedNumber(1234567890)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn build_ping_frame_with_data() {
|
|
||||||
let payload = b"attachment-data";
|
|
||||||
let bytes = build_ping_frame(99, "with-data", 555, payload).expect("encode failed");
|
|
||||||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
|
||||||
let tm = TypeMap::latest();
|
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
|
||||||
assert_eq!(cv.get_sender(), 99);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Description.to_id(&tm)),
|
|
||||||
&DataValue::Str("with-data".into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Timestamp.to_id(&tm)),
|
|
||||||
&DataValue::UnsignedNumber(555)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Id.to_id(&tm)),
|
|
||||||
&DataValue::Bytes(payload.to_vec())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn build_ping_frame_client_id_zero() {
|
|
||||||
let bytes = build_ping_frame(0, "zero-id", 0, &[]).expect("encode failed");
|
|
||||||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
|
||||||
assert_eq!(cv.get_sender(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn build_demo_message_roundtrip() {
|
|
||||||
// The demo KEM-encrypts to the host's bundle, so a real host keypair is
|
|
||||||
// required; the client keyring only needs its Ed25519 signing key.
|
|
||||||
let keyring = Keyring::generate();
|
|
||||||
let keyring_bytes = keyring.to_bytes();
|
|
||||||
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
|
|
||||||
|
|
||||||
let result = build_demo_message(7, &keyring_bytes, &host_bundle);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let bytes = result.unwrap();
|
|
||||||
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
|
|
||||||
let tm = TypeMap::latest();
|
|
||||||
|
|
||||||
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
|
|
||||||
assert_eq!(cv.get_sender(), 7);
|
|
||||||
assert_eq!(
|
|
||||||
cv.get_data(DataType::Description.to_id(&tm)),
|
|
||||||
&DataValue::Str("MTP WASM Demo".into())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn build_demo_message_invalid_keyring() {
|
|
||||||
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
|
|
||||||
let result = build_demo_message(1, b"not-a-valid-keyring", &host_bundle);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let err = result.unwrap_err();
|
|
||||||
assert!(err.as_string().unwrap().contains("invalid keyring"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn parse_auth_response_success() {
|
|
||||||
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
|
||||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
|
|
||||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
|
|
||||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
|
|
||||||
.to_bytes()
|
|
||||||
.expect("encode failed");
|
|
||||||
|
|
||||||
let result = parse_auth_response(&resp).expect("parse failed");
|
|
||||||
|
|
||||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.as_bool());
|
|
||||||
assert_eq!(connected, Some(true));
|
|
||||||
|
|
||||||
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.as_f64());
|
|
||||||
assert_eq!(id, Some(42.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn parse_auth_response_rejected() {
|
|
||||||
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
|
||||||
.to_bytes()
|
|
||||||
.expect("encode failed");
|
|
||||||
|
|
||||||
let result = parse_auth_response(&resp).expect("parse failed");
|
|
||||||
|
|
||||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.as_bool());
|
|
||||||
assert_eq!(connected, Some(false));
|
|
||||||
|
|
||||||
// rejected should have no assignedId
|
|
||||||
let has_id = js_sys::Reflect::has(&result, &"assignedId".into()).unwrap_or(false);
|
|
||||||
assert!(!has_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn parse_auth_response_with_signature() {
|
|
||||||
let sig_bytes = vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe];
|
|
||||||
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
|
|
||||||
.add_typed_default(DataType::Signature, DataValue::Bytes(sig_bytes.clone()))
|
|
||||||
.to_bytes()
|
|
||||||
.expect("encode failed");
|
|
||||||
|
|
||||||
let result = parse_auth_response(&resp).expect("parse failed");
|
|
||||||
|
|
||||||
let has_sig = js_sys::Reflect::has(&result, &"signature".into()).unwrap_or(false);
|
|
||||||
assert!(has_sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
|
||||||
fn parse_auth_response_invalid_frame() {
|
|
||||||
let result = parse_auth_response(b"garbage-data");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
wasm/src/subscription.rs
Normal file
44
wasm/src/subscription.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// Minimal message router used by higher-level SDK subscription code.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct WasmSubscriptionRouter {
|
||||||
|
handlers: HashMap<String, js_sys::Function>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl WasmSubscriptionRouter {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
handlers: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn subscribe(&mut self, message_type: String, callback: js_sys::Function) {
|
||||||
|
self.handlers.insert(message_type, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn unsubscribe(&mut self, message_type: &str) -> bool {
|
||||||
|
self.handlers.remove(message_type).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn dispatch(&self, message_type: &str, message: JsValue) -> bool {
|
||||||
|
let Some(callback) = self.handlers.get(message_type) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let _ = callback.call1(&JsValue::NULL, &message);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmSubscriptionRouter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,9 @@ use std::rc::Rc;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{WebTransport, WebTransportHash, WebTransportOptions};
|
|
||||||
|
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
|
use crate::frame::parse_frame_value;
|
||||||
|
|
||||||
const CLOSE_FRAME_LEN: u32 = u32::MAX;
|
const CLOSE_FRAME_LEN: u32 = u32::MAX;
|
||||||
|
|
||||||
|
|
@ -40,42 +40,6 @@ enum FrameOutcome {
|
||||||
Ended,
|
Ended,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Debug-log the exact bytes about to be written to the WebTransport stream.
|
|
||||||
*
|
|
||||||
* Wire layout (note the DOUBLE length prefix):
|
|
||||||
* [0..4] outer_len u32 BE - added by send_frame (= inner frame length)
|
|
||||||
* [4..8] inner_len u32 BE - added by CommunicationValue::to_bytes
|
|
||||||
* [8..10] comm_type u16 BE - e.g. Identification
|
|
||||||
* [10] flags u8
|
|
||||||
* [11..] id/sender/receiver/signature/data, gated by `flags`
|
|
||||||
*/
|
|
||||||
fn log_frame_bytes(wire: &[u8]) {
|
|
||||||
let hex: String = wire
|
|
||||||
.iter()
|
|
||||||
.map(|b| format!("{b:02x}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let outer_len = wire
|
|
||||||
.get(0..4)
|
|
||||||
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]));
|
|
||||||
let inner_len = wire
|
|
||||||
.get(4..8)
|
|
||||||
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]));
|
|
||||||
let comm_type = wire.get(8..10).map(|b| u16::from_be_bytes([b[0], b[1]]));
|
|
||||||
let flags = wire.get(10).copied();
|
|
||||||
|
|
||||||
web_sys::console::log_1(
|
|
||||||
&format!(
|
|
||||||
"mtp-wasm send_frame: {len} bytes | outer_len={outer_len:?} inner_len={inner_len:?} \
|
|
||||||
comm_type={comm_type:?} flags={flags:?}\n{hex}",
|
|
||||||
len = wire.len(),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* WebTransport client transport.
|
* WebTransport client transport.
|
||||||
*
|
*
|
||||||
|
|
@ -90,7 +54,7 @@ fn log_frame_bytes(wire: &[u8]) {
|
||||||
*/
|
*/
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WasmTransport {
|
pub struct WasmTransport {
|
||||||
inner: WebTransport,
|
inner: JsValue,
|
||||||
/// Reader over `incoming_unidirectional_streams()` (a singleton stream of streams).
|
/// Reader over `incoming_unidirectional_streams()` (a singleton stream of streams).
|
||||||
streams_reader: Rc<RefCell<Option<JsValue>>>,
|
streams_reader: Rc<RefCell<Option<JsValue>>>,
|
||||||
/// Reader over the host's current uni-directional stream, if one is open.
|
/// Reader over the host's current uni-directional stream, if one is open.
|
||||||
|
|
@ -101,28 +65,48 @@ pub struct WasmTransport {
|
||||||
|
|
||||||
impl WasmTransport {
|
impl WasmTransport {
|
||||||
pub async fn connect(url: &str, cert_hashes: Option<Vec<String>>) -> Result<Self, JsValue> {
|
pub async fn connect(url: &str, cert_hashes: Option<Vec<String>>) -> Result<Self, JsValue> {
|
||||||
let transport = match cert_hashes {
|
let ctor = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("WebTransport"))?
|
||||||
Some(hashes) => {
|
.dyn_into::<js_sys::Function>()
|
||||||
let opts = WebTransportOptions::new();
|
.map_err(|_| js_error("WebTransport not available"))?;
|
||||||
let mut wt_hashes = Vec::new();
|
let args = js_sys::Array::new();
|
||||||
for h in hashes {
|
args.push(&JsValue::from_str(url));
|
||||||
if let Some((algo, hex_val)) = h.split_once(':') {
|
|
||||||
if let Ok(bytes) = hex::decode(hex_val) {
|
if let Some(hashes) = cert_hashes {
|
||||||
let hash = WebTransportHash::new();
|
let wt_hashes = js_sys::Array::new();
|
||||||
hash.set_algorithm(algo);
|
for h in hashes {
|
||||||
hash.set_value_u8_array(&js_sys::Uint8Array::from(&bytes[..]));
|
if let Some((algo, hex_val)) = h.split_once(':') {
|
||||||
wt_hashes.push(hash);
|
if let Ok(bytes) = hex::decode(hex_val) {
|
||||||
}
|
let hash = js_sys::Object::new();
|
||||||
|
js_sys::Reflect::set(
|
||||||
|
&hash,
|
||||||
|
&JsValue::from_str("algorithm"),
|
||||||
|
&JsValue::from_str(algo),
|
||||||
|
)?;
|
||||||
|
js_sys::Reflect::set(
|
||||||
|
&hash,
|
||||||
|
&JsValue::from_str("value"),
|
||||||
|
&js_sys::Uint8Array::from(&bytes[..]),
|
||||||
|
)?;
|
||||||
|
wt_hashes.push(&hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !wt_hashes.is_empty() {
|
|
||||||
opts.set_server_certificate_hashes(&wt_hashes);
|
|
||||||
}
|
|
||||||
WebTransport::new_with_options(url, &opts)?
|
|
||||||
}
|
}
|
||||||
None => WebTransport::new(url)?,
|
if wt_hashes.length() > 0 {
|
||||||
|
let opts = js_sys::Object::new();
|
||||||
|
js_sys::Reflect::set(
|
||||||
|
&opts,
|
||||||
|
&JsValue::from_str("serverCertificateHashes"),
|
||||||
|
&wt_hashes,
|
||||||
|
)?;
|
||||||
|
args.push(&opts);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
JsFuture::from(transport.ready())
|
|
||||||
|
let transport = js_sys::Reflect::construct(&ctor, &args)?;
|
||||||
|
let ready = js_sys::Reflect::get(&transport, &JsValue::from_str("ready"))?
|
||||||
|
.dyn_into::<js_sys::Promise>()
|
||||||
|
.map_err(|_| js_error("WebTransport.ready is not a Promise"))?;
|
||||||
|
JsFuture::from(ready)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -133,12 +117,21 @@ impl WasmTransport {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inner(&self) -> &WebTransport {
|
pub fn inner(&self) -> &JsValue {
|
||||||
&self.inner
|
&self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
|
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
|
||||||
let stream_promise = self.inner.create_unidirectional_stream();
|
let create_stream = js_sys::Reflect::get(
|
||||||
|
&self.inner,
|
||||||
|
&JsValue::from_str("createUnidirectionalStream"),
|
||||||
|
)?
|
||||||
|
.dyn_into::<js_sys::Function>()
|
||||||
|
.map_err(|_| js_error("createUnidirectionalStream not a function"))?;
|
||||||
|
let stream_promise = create_stream
|
||||||
|
.call0(&self.inner)?
|
||||||
|
.dyn_into::<js_sys::Promise>()
|
||||||
|
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
|
||||||
let stream = JsFuture::from(stream_promise).await?;
|
let stream = JsFuture::from(stream_promise).await?;
|
||||||
|
|
||||||
let writable_or_stream = resolve_stream_writable(&stream)?;
|
let writable_or_stream = resolve_stream_writable(&stream)?;
|
||||||
|
|
@ -155,8 +148,6 @@ impl WasmTransport {
|
||||||
wire.extend_from_slice(&len.to_be_bytes());
|
wire.extend_from_slice(&len.to_be_bytes());
|
||||||
wire.extend_from_slice(frame);
|
wire.extend_from_slice(frame);
|
||||||
|
|
||||||
log_frame_bytes(&wire);
|
|
||||||
|
|
||||||
let chunk = js_sys::Uint8Array::from(&wire[..]);
|
let chunk = js_sys::Uint8Array::from(&wire[..]);
|
||||||
|
|
||||||
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
|
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
|
||||||
|
|
@ -185,7 +176,10 @@ impl WasmTransport {
|
||||||
if let Some(reader) = self.streams_reader.borrow().clone() {
|
if let Some(reader) = self.streams_reader.borrow().clone() {
|
||||||
return Ok(reader);
|
return Ok(reader);
|
||||||
}
|
}
|
||||||
let incoming = self.inner.incoming_unidirectional_streams();
|
let incoming = js_sys::Reflect::get(
|
||||||
|
&self.inner,
|
||||||
|
&JsValue::from_str("incomingUnidirectionalStreams"),
|
||||||
|
)?;
|
||||||
let reader = js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader"))
|
let reader = js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader"))
|
||||||
.map_err(|_| js_error("missing getReader"))?
|
.map_err(|_| js_error("missing getReader"))?
|
||||||
.dyn_into::<js_sys::Function>()
|
.dyn_into::<js_sys::Function>()
|
||||||
|
|
@ -341,10 +335,15 @@ impl WasmTransport {
|
||||||
pub async fn receive_loop(&self, on_message: js_sys::Function, on_error: js_sys::Function) {
|
pub async fn receive_loop(&self, on_message: js_sys::Function, on_error: js_sys::Function) {
|
||||||
loop {
|
loop {
|
||||||
match self.next_frame().await {
|
match self.next_frame().await {
|
||||||
Ok(FrameOutcome::Frame(frame)) => {
|
Ok(FrameOutcome::Frame(frame)) => match parse_frame_value(&frame) {
|
||||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
Ok(parsed) => {
|
||||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
let _ = on_message.call1(&JsValue::NULL, &parsed);
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let message = e.as_string().unwrap_or_else(|| format!("{:?}", e));
|
||||||
|
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str(&message));
|
||||||
|
}
|
||||||
|
},
|
||||||
Ok(FrameOutcome::Closed) | Ok(FrameOutcome::Ended) => break,
|
Ok(FrameOutcome::Closed) | Ok(FrameOutcome::Ended) => break,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = on_error.call1(&JsValue::NULL, &e);
|
let _ = on_error.call1(&JsValue::NULL, &e);
|
||||||
|
|
@ -355,7 +354,10 @@ impl WasmTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&self) {
|
pub fn close(&self) {
|
||||||
let info = web_sys::WebTransportCloseInfo::new();
|
if let Ok(close) = js_sys::Reflect::get(&self.inner, &JsValue::from_str("close"))
|
||||||
let _ = self.inner.close_with_close_info(&info);
|
.and_then(|value| value.dyn_into::<js_sys::Function>().map_err(Into::into))
|
||||||
|
{
|
||||||
|
let _ = close.call1(&self.inner, &js_sys::Object::new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
173
wasm/types/mtp_wasm.d.ts
vendored
Normal file
173
wasm/types/mtp_wasm.d.ts
vendored
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisposableWasmObject {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StateChangeCallback = (state: ConnectionState) => void;
|
||||||
|
export type MessageCallback = (frame: ParsedFrame) => void;
|
||||||
|
export type ErrorCallback = (error: string) => void;
|
||||||
|
|
||||||
|
export interface Ed25519GenerateResult {
|
||||||
|
signer: WasmEd25519Signer;
|
||||||
|
secretKey: Uint8Array;
|
||||||
|
publicKey: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
connected: boolean;
|
||||||
|
clientNonce?: Uint8Array;
|
||||||
|
assignedId?: number;
|
||||||
|
timestamp?: number;
|
||||||
|
signature?: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedResponse {
|
||||||
|
_id?: number;
|
||||||
|
_type: string;
|
||||||
|
[field: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedFrame {
|
||||||
|
id?: number;
|
||||||
|
type: string;
|
||||||
|
sender?: bigint;
|
||||||
|
receiver?: bigint;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
raw: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConnectionConfig implements DisposableWasmObject {
|
||||||
|
constructor(url: string);
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
client_id: bigint;
|
||||||
|
server_certificate_hashes: string[];
|
||||||
|
readonly url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConnectionState {
|
||||||
|
Disconnected = 0,
|
||||||
|
Connecting = 1,
|
||||||
|
Connected = 2,
|
||||||
|
Failed = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WasmLogHint {
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Error = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmChaCha20Poly1305 implements DisposableWasmObject {
|
||||||
|
constructor(key: Uint8Array);
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
decrypt(ciphertext: Uint8Array, aad: Uint8Array): Uint8Array;
|
||||||
|
encrypt(plaintext: Uint8Array, aad: Uint8Array): Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmClient implements DisposableWasmObject {
|
||||||
|
constructor(
|
||||||
|
on_state_change: StateChangeCallback,
|
||||||
|
on_message: MessageCallback,
|
||||||
|
on_error: ErrorCallback,
|
||||||
|
);
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
auth_connect(
|
||||||
|
config: ConnectionConfig,
|
||||||
|
host_public_key_bytes: Uint8Array,
|
||||||
|
keyring_bytes: Uint8Array,
|
||||||
|
client_id: bigint,
|
||||||
|
): Promise<bigint>;
|
||||||
|
auth_register(
|
||||||
|
config: ConnectionConfig,
|
||||||
|
host_public_key_bytes: Uint8Array,
|
||||||
|
keyring_bytes: Uint8Array,
|
||||||
|
): Promise<bigint>;
|
||||||
|
connect(config: ConnectionConfig): Promise<void>;
|
||||||
|
disconnect(): void;
|
||||||
|
request(frame: Uint8Array, response_type?: string | null): Promise<ParsedFrame>;
|
||||||
|
send(frame: Uint8Array): Promise<void>;
|
||||||
|
start_protocol_pings(interval_ms: number, client_id: bigint): void;
|
||||||
|
stop_protocol_pings(): void;
|
||||||
|
subscribe(message_type: string, callback: MessageCallback): number;
|
||||||
|
unsubscribe(id: number): boolean;
|
||||||
|
static is_supported(): boolean;
|
||||||
|
readonly state: ConnectionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmEd25519Signer implements DisposableWasmObject {
|
||||||
|
constructor(secret_key: Uint8Array);
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
sign(message: Uint8Array): Uint8Array;
|
||||||
|
verify(message: Uint8Array, signature: Uint8Array): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmKeyring implements DisposableWasmObject {
|
||||||
|
private constructor();
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
static from_bytes(bytes: Uint8Array): WasmKeyring;
|
||||||
|
public_key_bundle(): WasmPublicKeyBundle;
|
||||||
|
to_bytes(): Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmPublicKeyBundle implements DisposableWasmObject {
|
||||||
|
private constructor();
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
static from_bytes(bytes: Uint8Array): WasmPublicKeyBundle;
|
||||||
|
to_bytes(): Uint8Array;
|
||||||
|
readonly kem_public_key: Uint8Array;
|
||||||
|
readonly sig_cl_public_key: Uint8Array;
|
||||||
|
readonly sig_pq_public_key: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WasmSubscriptionRouter implements DisposableWasmObject {
|
||||||
|
constructor();
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
dispatch(message_type: string, message: unknown): boolean;
|
||||||
|
subscribe(message_type: string, callback: (message: unknown) => void): void;
|
||||||
|
unsubscribe(message_type: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function build_ping_frame(
|
||||||
|
client_id: bigint,
|
||||||
|
description: string,
|
||||||
|
timestamp: bigint,
|
||||||
|
data: Uint8Array,
|
||||||
|
): Uint8Array;
|
||||||
|
|
||||||
|
export function build_frame(message_type: string, data: Record<string, unknown>, options?: {
|
||||||
|
id?: number;
|
||||||
|
sender?: bigint | number;
|
||||||
|
receiver?: bigint | number;
|
||||||
|
}): Uint8Array;
|
||||||
|
|
||||||
|
export function ed25519_generate(): Ed25519GenerateResult;
|
||||||
|
export function ed25519_verify(public_key: Uint8Array, message: Uint8Array, signature: Uint8Array): void;
|
||||||
|
export function format_frame(frame: Uint8Array): string;
|
||||||
|
export function keyring_from_ed25519(secret_key: Uint8Array, public_key: Uint8Array): Uint8Array;
|
||||||
|
export function main(): void;
|
||||||
|
export function parse_auth_response(response: Uint8Array): AuthResponse;
|
||||||
|
export function parse_frame(frame: Uint8Array): ParsedFrame;
|
||||||
|
export function wasm_derive_encryption_key(ikm: Uint8Array, salt: Uint8Array, context: Uint8Array): Uint8Array;
|
||||||
|
export function wasm_hkdf_expand(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, len: number): Uint8Array;
|
||||||
|
export function wasm_sha256(data: Uint8Array): Uint8Array;
|
||||||
|
export function wasm_sha256_double(data: Uint8Array): Uint8Array;
|
||||||
|
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
export default function init(
|
||||||
|
module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>,
|
||||||
|
): Promise<InitOutput>;
|
||||||
Loading…
Reference in a new issue