oficina / docs Public

core: fix 1 - zero-import gate and locked build

Address review findings:
1. Add zero-import test at core/tests/no_host_imports.rs using wasmparser
   to verify wasm has no host imports. Gracefully skip if artifact absent;
   fail with import names if any found. This gates against the current
   doc-tree: yrs -> fastrand -> getrandom(wasm_js) -> wasm-bindgen.

2. Add crate-level doc comment recording the constraint: never call
   yrs::Doc::new() — always use Doc::with_client_id(). Doc::new() pulls
   random entropy via fastrand, which is dead-stripped today but would
   break the gate when called. Verified Doc::with_client_id exists.

3. Exact-pin yrs and wasmparser with "=" in Cargo.toml and add --locked
   to scripts/build-wasm.sh to enforce Cargo.lock versions.

Test both paths: cargo test (skips before artifact), then build-wasm.sh,
then cargo test again (passes). Artifact remains 386 bytes.
Paul Campbell pushed by paul@keymail.dev b6a5c09d6c9afdadd328327251ef732e3e96cf75 parent 3dc3a70
5 files changed, +107 −3
  • core/Cargo.lock +56 −1
  • core/Cargo.toml +4 −1
  • core/src/lib.rs +7 −0
  • core/tests/no_host_imports.rs +39 −0
  • scripts/build-wasm.sh +1 −1
diff --git a/core/Cargo.lock b/core/Cargo.lock
index fb1aa4e..341d10e 100644
--- a/core/Cargo.lock
+++ b/core/Cargo.lock
@@ -65,7 +65,7 @@ checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
- "hashbrown",
+ "hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
@@ -75,9 +75,16 @@ dependencies = [
name = "docs-core"
version = "0.1.0"
dependencies = [
+ "wasmparser",
"yrs",
]
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
[[package]]
name = "event-listener"
version = "5.4.2"
@@ -107,6 +114,12 @@ dependencies = [
"getrandom",
]
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "getrandom"
version = "0.4.3"
@@ -126,6 +139,29 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "foldhash",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "itoa"
version = "1.0.18"
@@ -239,6 +275,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -390,6 +432,19 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasmparser"
+version = "0.258.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.17.1",
+ "indexmap",
+ "semver",
+ "serde",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 748f7a1..1352fe6 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -7,9 +7,12 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
-yrs = "0.23.5"
+yrs = "=0.23.5"
[profile.release]
opt-level = "s"
lto = true
panic = "abort"
+
+[dev-dependencies]
+wasmparser = "=0.258.0"
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 588df2d..d9bde7d 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -1,5 +1,12 @@
//! The Docs core: Yjs CRDT and (later) text layout, compiled to wasm and
//! run both under wazero on the server and — for layout only — in the browser.
+//!
+//! # Important: Client ID initialization
+//!
+//! Never call `yrs::Doc::new()` in this crate — always use `Doc::with_client_id()`.
+//! `Doc::new()` internally reaches for random entropy via fastrand, which pulls a JS
+//! import that the Go/wazero host cannot provide. Providing an explicit client ID
+//! avoids this and aligns with the design, which assigns client IDs deterministically.
/// Returns the ABI version this module exports. The Go host checks this on
/// load and refuses a module it does not understand, so a stale .wasm on a
diff --git a/core/tests/no_host_imports.rs b/core/tests/no_host_imports.rs
new file mode 100644
index 0000000..d9e849e
--- /dev/null
+++ b/core/tests/no_host_imports.rs
@@ -0,0 +1,39 @@
+use std::path::Path;
+
+#[test]
+fn no_host_imports() {
+ let wasm_path = "target/wasm32-unknown-unknown/release/docs_core.wasm";
+
+ // Skip if artifact not yet built
+ if !Path::new(wasm_path).exists() {
+ eprintln!(
+ "SKIPPED: wasm artifact not found at {}. Run ./scripts/build-wasm.sh first.",
+ wasm_path
+ );
+ return;
+ }
+
+ let wasm_bytes = std::fs::read(wasm_path).expect("Failed to read wasm file");
+ let parser = wasmparser::Parser::new(0);
+
+ let mut imports = Vec::new();
+
+ for payload in parser.parse_all(&wasm_bytes) {
+ let payload = payload.expect("Failed to parse wasm module");
+
+ if let wasmparser::Payload::ImportSection(reader) = payload {
+ let import_reader = reader.into_imports();
+ for import_result in import_reader {
+ let import = import_result.expect("Failed to parse import");
+ imports.push(format!("{}.{}", import.module, import.name));
+ }
+ }
+ }
+
+ if !imports.is_empty() {
+ panic!(
+ "WASM module has host imports (breaks Go/wazero compatibility):\n {}",
+ imports.join("\n ")
+ );
+ }
+}
diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh
index db0d9bc..df8d4eb 100755
--- a/scripts/build-wasm.sh
+++ b/scripts/build-wasm.sh
@@ -3,7 +3,7 @@
# provides no JS glue, so the module must be plain wasm32 with a C ABI.
set -euo pipefail
cd "$(dirname "$0")/.."
-cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown
+cargo build --release --manifest-path core/Cargo.toml --target wasm32-unknown-unknown --locked
out="core/target/wasm32-unknown-unknown/release/docs_core.wasm"
[ -f "$out" ] || { echo "build-wasm: no artifact at $out" >&2; exit 1; }
echo "$out ($(wc -c < "$out") bytes)"