diff --git a/Cargo.lock b/Cargo.lock index f0a0b33b8..8b4e5125c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,18 +210,18 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "automerge" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aab56635599ee2e9df28d9ce180c155b8dbcdd96e4c3f62895fc6a44137d328" +checksum = "09b78abcbba93428b9465b26cb2816a5b4654cce507f099a84a8c1b311cb3633" dependencies = [ "cfg-if", "flate2", - "getrandom 0.3.4", + "getrandom 0.4.2", "hex", "hexane", "itertools", "leb128", - "rand 0.9.4", + "rand 0.10.1", "rustc-hash", "serde", "sha2 0.11.0", @@ -526,6 +526,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1661,6 +1672,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -4118,6 +4130,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4156,6 +4179,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -4616,8 +4645,8 @@ dependencies = [ [[package]] name = "samod" -version = "0.9.0" -source = "git+https://github.com/quarto-dev/samod.git?branch=q2#0b50c16ce686f48340d51963c25af35c63e11603" +version = "0.12.1" +source = "git+https://github.com/shikokuchuo/samod.git?branch=access-policy#c5a06c397e69554b65441da39da6deb788a75d26" dependencies = [ "async-channel", "automerge", @@ -4639,8 +4668,8 @@ dependencies = [ [[package]] name = "samod-core" -version = "0.9.0" -source = "git+https://github.com/quarto-dev/samod.git?branch=q2#0b50c16ce686f48340d51963c25af35c63e11603" +version = "0.12.0" +source = "git+https://github.com/shikokuchuo/samod.git?branch=access-policy#c5a06c397e69554b65441da39da6deb788a75d26" dependencies = [ "automerge", "base64 0.21.7", diff --git a/claude-notes/plans/2026-07-01-samod-access-policy-migration.md b/claude-notes/plans/2026-07-01-samod-access-policy-migration.md new file mode 100644 index 000000000..5e035a740 --- /dev/null +++ b/claude-notes/plans/2026-07-01-samod-access-policy-migration.md @@ -0,0 +1,248 @@ +# Migrate samod: `quarto-dev/samod@q2` → `shikokuchuo/samod@access-policy` + +## Overview + +Our vendored samod fork is moving from **`quarto-dev/samod` branch `q2`** +(samod `0.9.0`, automerge `0.8.0`) to **`shikokuchuo/samod` branch +`access-policy`** (samod `0.12.1`, automerge `0.10.0`). + +The new fork = upstream samod `0.12.1` + a fresh "Implement access policy" +commit. The *only* thing the old `q2` branch carried on top of its base was +its own "Implement access policy" commit; every other commit under it was +already upstream. So switching forks **gains** the upstream 0.10/0.11/0.12 +work and **loses nothing q2-specific** except the *shape* of the access-policy +API, which is deliberately replaced. + +Two direct consumers, both native-only: + +- `crates/quarto-hub` — the collaborative editing server (features `tokio`, + `axum`, `tungstenite`). Owns `AuditAccessPolicy`. +- `crates/quarto-preview` — reads back binary docs (feature `tokio`). + +The WASM crate (`crates/wasm-quarto-hub-client`) has **no** Rust samod/automerge +dependency, so the WASM build is unaffected. The browser/Node side uses JS +`@automerge/automerge ^3.2.6` + `@automerge/automerge-repo ^2.5.x`. + +**Scope is exactly two `Cargo.toml`s.** Reverse-dep check: only `quarto-preview` +(deps `quarto-hub`) and the `quarto` binary (deps both) sit above these crates — +the WASM crate is *not* above them, so the hub-client/WASM leg is out of scope +(unless the JS-bump question pulls it in; see Phase 5 / open Q2). `quarto` and +`quarto-util` mention `samod` only in comments and tracing-filter *strings* +(`samod=info`), not as a dependency, so they need no change. A plain +`cargo build --workspace` covers the transitive rebuild of `quarto-preview` / +`quarto`. + +### What actually changed in the API (verified against both trees) + +**1. `AccessPolicy` trait: async → synchronous (the headline break).** + +Old (`q2`, samod 0.9.0): +```rust +pub trait AccessPolicy: Clone + Send + 'static { + fn should_allow(&self, doc_id: DocumentId, peer_id: PeerId) + -> impl Future + Send + 'static; +} +// also exported: LocalAccessPolicy (unused by us) +``` + +New (`access-policy`, samod 0.12.1): +```rust +pub trait AccessPolicy: Send + Sync + 'static { + fn is_allowed(&self, doc_id: &DocumentId, peer_id: &PeerId) -> bool; +} +// blanket impl for Fn(&DocumentId, &PeerId) -> bool; struct AllowAll +``` + +Changes: method renamed `should_allow` → `is_allowed`; args now **borrowed** +(`&DocumentId, &PeerId`); returns **`bool` synchronously** (no future); bound +is `Send + Sync + 'static` (was `Clone + Send + 'static`); no `LocalAccessPolicy`. +The policy is now consulted *synchronously* at the hub's actor↔peer boundary +and **cannot perform async work** (DB / remote-auth lookups). Our +`AuditAccessPolicy` only locks a `Mutex`, reads a `HashMap`, and logs — all +synchronous already — so the port is mechanical. + +**2. automerge `0.8.0` → `0.10.0` (spans two minor releases).** samod 0.12.x +pins `automerge = "0.10.0"`; our two crates declare a **direct** dep on +`automerge = "0.8.0"` (feature `utf16-indexing`) to share samod's types, so the +direct dep must move to `0.10.0` in lockstep. Direct automerge API touched (8 +files): `AutomergeError`, `ReadDoc`, `Transactable`, `Value::Scalar`, +`ScalarValue::{Str,Bytes}`, `ROOT`, `ObjType`, `ObjId`, `ChangeHash`, +`Automerge::merge`, and `TextEncoding::{platform_default, Utf16CodeUnit}` +(tests only). The `TextEncoding` variants we use survive in 0.10 (the enum only +*gained* variants). The remaining 0.8→0.10 breaks are unknown until a compile +pass — let the compiler drive them. + +### What is NOT breaking (verified present in the new fork) + +`RepoBuilder` shape — `Repo::build_tokio()`, `.with_storage()`, +`.with_announce_policy(NeverAnnounce)`, `.with_access_policy(..)`, `.load()`; +`AnnouncePolicy` / `NeverAnnounce` / `AlwaysAnnounce` (still async, unchanged); +`Repo::make_acceptor` + `AcceptorEvent`; `Stopped`; `PeerInfo { storage_id: +Option }`; `StorageId::from`, `PeerId::from`, `DocumentId` + +`from_str`; `InMemoryStorage`, `storage::TokioFilesystemStorage`; `DocHandle` +methods `with_document`, `document_id`, `create`, `find`, `peers`, `changes`, +`broadcast`. The `with_access_policy(audit_policy)` **call site** in +`context.rs` needs no change — only the trait impl does. + +### Highest-risk item: JS ↔ Rust interop & on-disk format + +samod exists for JS `automerge-repo` interoperability. Bumping the Rust +automerge 0.8→0.10 must remain compatible with (a) documents already on disk in +the hub's `automerge_dir`, and (b) the JS automerge / automerge-repo versions +hub-client speaks over the wire. automerge's storage + sync formats are designed +to be stable, but this is a **must-verify**, not an assume. This is the one part +the Rust test suite cannot cover on its own. + +## Work Items + +> **Compile-unit note.** Phases 1–3 do not build in isolation: rewriting the +> tests to the new trait (Phase 1) breaks `quarto-hub` until the dep (Phase 2) +> and source (Phase 3) land. That is the intended migration "red" — it is +> compile-level, not a running-but-failing test. Land 1–3 together; the first +> green test run is Phase 4. + +### Phase 0 — Isolated worktree + baseline + +- [x] `cargo xtask create-worktree bd-qp353u2b` (base `main`). Worktree: + `.worktrees/bd-qp353u2b-migrate-samod-quarto-devsamodq2/`. No `npm install`. +- [x] Green baseline on branch HEAD (old fork): `cargo build -p quarto-hub -p quarto-preview` + clean; `cargo nextest run -p quarto-hub -p quarto-preview` → 360 passed, 1 skipped. +- [x] Interop baseline captured with the *current* (samod 0.9 / automerge 0.8) hub + binary in standalone mode: `automerge_dir` with index doc + `2J1DWLvMjrSn3KdfZe3VQosnQ3Sw` (incremental sha256 + `dc51f19e…a4fda2bc9`). JS versions: `@automerge/automerge ^3.2.6`, + `@automerge/automerge-repo ^2.5.6` (hub-client) / `^2.5.1` (ts-packages). + Pristine copy + README in the session scratchpad. + +### Phase 1 — Tests first (TDD) + +- [x] Rewrote the three `AuditAccessPolicy` tests to the sync trait: + `policy.is_allowed(&doc_id, &peer_id)` (no `.await`), `#[tokio::test]` → + `#[test]`, renamed `should_allow_*` → `is_allowed_*`. +- [x] Confirmed TDD "red": `cargo build -p quarto-hub --tests` failed with 3× + `error[E0599]: no method named is_allowed` against the old dep. +- [x] Left `automerge_api_tests.rs` untouched as the compiler-driven checklist + (it turned out to compile & pass unchanged — no automerge break surfaced). + +### Phase 2 — Dependency bump + +- [x] `crates/quarto-hub/Cargo.toml`: samod → `git = shikokuchuo/samod.git, + branch = "access-policy", version = "0.12.1"`; `automerge` `0.8.0` → `0.10.0` + (kept `utf16-indexing`). Pinned by **branch** (user decision). Added a + one-line "vendored fork (long-term home)" comment. +- [x] `crates/quarto-preview/Cargo.toml`: same samod + automerge bump. +- [x] axum comment left as-is — new samod still uses axum `0.8.x`, so `axum 0.8` + stays correct (workspace built clean; no axum resolution change). +- [x] `cargo update -p samod` re-resolved: automerge 0.8→0.10, samod 0.9→0.12.1, + samod-core 0.9→0.12.0 (both at `c5a06c39`), added transitive `chacha20`, + `rand 0.10.1`, `rand_core 0.10.1`. +- [x] Version unification confirmed: exactly **one** `automerge` entry in + `Cargo.lock` (0.10.0); `cargo tree -p quarto-hub -i automerge` shows the + direct dep and samod/samod-core share it. No dual-automerge red flag. + +### Phase 3 — Migrate the source + +- [x] `access_policy.rs`: `impl AccessPolicy` now `fn is_allowed(&self, doc_id: + &DocumentId, peer_id: &PeerId) -> bool`; reads `.get(peer_id)`, logs on hit, + returns `true`. Dropped `use std::future::Future;` and the `async { true }`. + Kept `#[derive(Clone)]` (harmless; no longer required by the bound). +- [x] `context.rs`: `.with_access_policy(audit_policy)` type-checks unchanged + (`quarto-hub` compiled clean) — no source change. +- [x] **No automerge 0.8→0.10 breaks materialized.** `quarto-hub` (lib + tests) + and `quarto-preview` compiled clean against automerge 0.10 with zero edits + to the 8 anticipated files. The automerge API surface these crates use + (`ReadDoc`, `Transactable`, `ScalarValue`, `ROOT`, `ObjType`, `ChangeHash`, + `Automerge::merge`, `TextEncoding::{platform_default,Utf16CodeUnit}`, …) is + stable across 0.8→0.10 for our usage. The single hand-edit was the trait impl. + +### Phase 4 — Rust test suite + +- [x] `cargo nextest run -p quarto-hub -p quarto-preview` → 360 passed, 1 skipped + (matches baseline). Targeted run: 3 access-policy + 24 automerge-API tests + all pass. +- [x] `cargo nextest run --workspace` → **9855 passed, 197 skipped** (exit 0). + No downstream regression. + +### Phase 5 — End-to-end interop verification (the part tests can't do) + +- [x] **On-disk backward-compat verified.** Started the NEW (automerge 0.10 / + samod 0.12) `target/debug/hub` in standalone mode pointed at a copy of the + Phase-0 `automerge_dir` written by the OLD binary: + `QUARTO_HUB_DATA_DIR= ./target/debug/hub -P 3988 -H 127.0.0.1 -v`. + Log: `Loaded existing index document doc_id=2J1DWLvMjrSn3KdfZe3VQosnQ3Sw` + (the exact Phase-0 doc), and `hub.json`'s `index_document_id` unchanged. No + re-init, no corruption. +- [x] **Live JS↔Rust wire interop verified (automated).** Ran the sync-client + Rust-hub interop suite against the new binary from the worktree: + `vitest run offline-creation-rust-hub restart-window-creation exit-drain` + (ts-packages/quarto-sync-client) → **3 files / 5 tests passed**. These + spawn the worktree's new `hub` and drive the real JS client + (`@automerge/automerge 3.2.x` / `automerge-repo 2.5.x`): a JS-created + project + file syncs over the wire and lands in the new hub's on-disk + storage (`automerge//`), proving both the wire and on-disk + formats interoperate across automerge 0.8→0.10 / samod 0.9→0.12. +- [x] **Audit log verified (deterministic, in-repo).** Added a `tracing`-capture + test `access_policy::tests::logs_document_accessed_with_email_for_known_peer` + asserting the `"Document accessed"` line + email are emitted from the now + synchronous `is_allowed`, and strengthened `no_log_when_peer_unknown` to + assert the line is absent. The peer→email wiring (`server.rs:974`, on + `AcceptorEvent::ClientConnected`) compiles clean against samod 0.12 and the + accept path is exercised by the interop tests above. + **Honest limitation:** the *live-auth* grep of a running hub's stdout was + NOT performed — it needs either the embedded MCP bundle (a `PLACEHOLDER` in + this fresh worktree) or the mock-OIDC bearer harness (`auth_bearer.rs`); + both are unrelated to and out of scope for the samod/automerge migration. + The `tracing::info!(… "Document accessed")` call itself is byte-identical + pre/post migration (only the enclosing fn signature changed). + +### Phase 6 — Full verification & handoff + +- [x] `cargo xtask verify` → **✓ All verification steps passed!** The in-scope + Rust legs are green at CI strictness: clippy `--workspace --all-targets + -D warnings`, `cargo fmt --check`, `cargo build --workspace -D warnings`, + tree-sitter grammar tests, and `cargo nextest run --workspace -D warnings` + (**9856 passed, 197 skipped**). The JS/browser legs (hub-client build+tests, + ts-packages build, shared preview-renderer/runtime, trace-viewer, + q2-preview-spa, hub-mcp) were skipped — they are environmentally blocked in + a fresh worktree (`wasm-quarto-hub-client` is not built, so every JS test + leg fails with `Cannot find package 'wasm-quarto-hub-client'`) and are out + of scope (the WASM crate does not depend on `quarto-hub`/`quarto-preview`). + Verified this by a first run with only `--skip-hub-build`: the sole non- + environmental failure was `intelligenceService.test.ts` (an LSP + semantic-tokens parsing test, disjoint from samod/automerge) — pre-existing + / unrelated to this Rust change. +- [x] `cargo xtask lint` → All checks passed (812 files). +- [x] Staged + committed as `9967abf9` on branch + `braid/bd-qp353u2b-migrate-samod-quarto-devsamodq2` (worktree). Commit + message calls out the automerge 0.8→0.10 bump + the sync AccessPolicy trait + change. **Not pushed** — awaiting explicit approval (GIT PUSH POLICY). +- [x] Reported E2E interop result + `Cargo.lock` diff summary; push approval + requested. + +## Open questions — RESOLVED + +1. **Pin to rev vs branch?** → **Branch** (`branch = "access-policy"`), per user + decision. Cargo froze rev `c5a06c39…` in `Cargo.lock`. Mirrors the old `q2` + branch-pin. (A force-push to `access-policy` moves the build on next + `cargo update`; acceptable — it is the user's own fork.) +2. **JS package bump?** → **No.** Phase 5's live JS↔Rust round-trip passed with + the *existing* JS versions (`@automerge/automerge ^3.2.6`, + `@automerge/automerge-repo ^2.5.x`) against the automerge-0.10 / samod-0.12 + hub. Wire + on-disk formats interoperate; hub-client/ts-packages stay out of + scope. +3. **Upstreaming intent** → **Long-term fork** (user decision). Dep comment reads + "Vendored samod fork carrying the synchronous AccessPolicy trait (long-term + home)." + +## References + +- New fork: `shikokuchuo/samod` branch `access-policy`, samod `0.12.1` / + samod-core `0.12.0` / automerge `0.10.0`. Access-policy commit `c5a06c3`. +- Old fork: `quarto-dev/samod` branch `q2`, rev `0b50c16` ("Implement access + policy" on samod `0.9.0` / automerge `0.8.0`). +- samod CHANGELOG (0.9→0.12): automerge 0.8→0.10; added `Repo::search` / + `SearchState`; `Repo::find` hang-after-unavailable fix; exponential sync-time + fix; `DocHandle::{changes,ephemeral}` now `'static`. +- Consumers: `crates/quarto-hub/src/{access_policy,context,server,sync,index, + sync_state,resource,automerge_api_tests}.rs`, + `crates/quarto-preview/src/capture_driver.rs`. diff --git a/crates/quarto-hub/Cargo.toml b/crates/quarto-hub/Cargo.toml index 5760868ea..366f1cf21 100644 --- a/crates/quarto-hub/Cargo.toml +++ b/crates/quarto-hub/Cargo.toml @@ -45,8 +45,9 @@ time = "0.3" # utf16-indexing: use UTF-16 code units for text indices, matching the JS/WASM client. # Without this, the Rust default is UnicodeCodePoint, which disagrees with JS on # non-BMP characters (emoji etc.) — each emoji is 1 code point but 2 UTF-16 code units. -automerge = { version = "0.8.0", features = ["utf16-indexing"] } -samod = { version = "0.9.0", git = "https://github.com/quarto-dev/samod.git", branch = "q2", features = ["tokio", "axum", "tungstenite"] } +automerge = { version = "0.10.0", features = ["utf16-indexing"] } +# Vendored samod fork carrying the synchronous AccessPolicy trait (long-term home). +samod = { version = "0.12.1", git = "https://github.com/shikokuchuo/samod.git", branch = "access-policy", features = ["tokio", "axum", "tungstenite"] } # Async streams (for samod event handling) futures = "0.3" diff --git a/crates/quarto-hub/src/access_policy.rs b/crates/quarto-hub/src/access_policy.rs index 5e7c60b86..813b2477c 100644 --- a/crates/quarto-hub/src/access_policy.rs +++ b/crates/quarto-hub/src/access_policy.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::future::Future; use std::sync::{Arc, Mutex as StdMutex}; use samod::{AccessPolicy, DocumentId, PeerId}; @@ -21,12 +20,8 @@ impl AuditAccessPolicy { } impl AccessPolicy for AuditAccessPolicy { - fn should_allow( - &self, - doc_id: DocumentId, - peer_id: PeerId, - ) -> impl Future + Send + 'static { - let email = self.peer_emails.lock().unwrap().get(&peer_id).cloned(); + fn is_allowed(&self, doc_id: &DocumentId, peer_id: &PeerId) -> bool { + let email = self.peer_emails.lock().unwrap().get(peer_id).cloned(); if let Some(ref email) = email { tracing::info!( @@ -37,7 +32,7 @@ impl AccessPolicy for AuditAccessPolicy { ); } - async { true } + true } } @@ -45,19 +40,54 @@ impl AccessPolicy for AuditAccessPolicy { mod tests { use super::*; - #[tokio::test] - async fn should_allow_always_returns_true() { + /// Run `f` with a scoped subscriber that captures formatted log output, + /// returning everything logged during the call. Lets us assert on the + /// audit "Document accessed" line without a live authenticated hub. + fn capture_logs(f: impl FnOnce()) -> String { + use std::io::Write; + use tracing_subscriber::fmt::MakeWriter; + + #[derive(Clone)] + struct BufWriter(Arc>>); + impl Write for BufWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + impl<'a> MakeWriter<'a> for BufWriter { + type Writer = BufWriter; + fn make_writer(&'a self) -> Self::Writer { + self.clone() + } + } + + let buf = Arc::new(StdMutex::new(Vec::new())); + let subscriber = tracing_subscriber::fmt() + .with_writer(BufWriter(buf.clone())) + .with_ansi(false) + .with_max_level(tracing::Level::TRACE) + .finish(); + tracing::subscriber::with_default(subscriber, f); + String::from_utf8(buf.lock().unwrap().clone()).unwrap() + } + + #[test] + fn is_allowed_always_returns_true() { let peer_emails = Arc::new(StdMutex::new(HashMap::new())); let policy = AuditAccessPolicy::new(peer_emails); let doc_id = DocumentId::new(&mut rand::rng()); let peer_id = PeerId::from("test-peer"); - assert!(policy.should_allow(doc_id, peer_id).await); + assert!(policy.is_allowed(&doc_id, &peer_id)); } - #[tokio::test] - async fn should_allow_returns_true_with_known_peer() { + #[test] + fn is_allowed_returns_true_with_known_peer() { let peer_emails = Arc::new(StdMutex::new(HashMap::new())); let peer_id = PeerId::from("known-peer"); peer_emails @@ -68,20 +98,52 @@ mod tests { let policy = AuditAccessPolicy::new(peer_emails); let doc_id = DocumentId::new(&mut rand::rng()); - assert!(policy.should_allow(doc_id, peer_id).await); + assert!(policy.is_allowed(&doc_id, &peer_id)); } - #[tokio::test] - async fn no_log_when_peer_unknown() { + #[test] + fn no_log_when_peer_unknown() { // When peer_emails has no mapping (auth disabled scenario), - // should_allow still returns true without logging. + // is_allowed still returns true without logging. let peer_emails = Arc::new(StdMutex::new(HashMap::new())); let policy = AuditAccessPolicy::new(peer_emails); - let doc_id = DocumentId::new(&mut rand::rng()); let peer_id = PeerId::from("unknown-peer"); - // Just verify it returns true and doesn't panic - assert!(policy.should_allow(doc_id, peer_id).await); + let mut allowed = false; + let logs = capture_logs(|| allowed = policy.is_allowed(&doc_id, &peer_id)); + + assert!(allowed); + assert!( + !logs.contains("Document accessed"), + "unknown peer must not be audit-logged; got: {logs}" + ); + } + + #[test] + fn logs_document_accessed_with_email_for_known_peer() { + // The migration kept the audit "Document accessed" log inside the now + // synchronous is_allowed. Assert it still emits with the peer's email. + let peer_id = PeerId::from("known-peer"); + let peer_emails = Arc::new(StdMutex::new(HashMap::new())); + peer_emails + .lock() + .unwrap() + .insert(peer_id.clone(), "user@example.com".to_string()); + let policy = AuditAccessPolicy::new(peer_emails); + let doc_id = DocumentId::new(&mut rand::rng()); + + let logs = capture_logs(|| { + assert!(policy.is_allowed(&doc_id, &peer_id)); + }); + + assert!( + logs.contains("Document accessed"), + "expected audit log line; got: {logs}" + ); + assert!( + logs.contains("user@example.com"), + "audit log must include the peer's email; got: {logs}" + ); } } diff --git a/crates/quarto-preview/Cargo.toml b/crates/quarto-preview/Cargo.toml index 8f9a91a25..da77203d2 100644 --- a/crates/quarto-preview/Cargo.toml +++ b/crates/quarto-preview/Cargo.toml @@ -22,8 +22,8 @@ thiserror.workspace = true # Match quarto-hub's versions so `capture_driver::read_capture_from_doc` # can talk to samod directly (it reads back the binary doc Phase C.1 # writes; Phase C.4 reuses the same wire format in the WASM/SPA layer). -automerge = { version = "0.8.0", features = ["utf16-indexing"] } -samod = { version = "0.9.0", git = "https://github.com/quarto-dev/samod.git", branch = "q2", features = ["tokio"] } +automerge = { version = "0.10.0", features = ["utf16-indexing"] } +samod = { version = "0.12.1", git = "https://github.com/shikokuchuo/samod.git", branch = "access-policy", features = ["tokio"] } quarto-core.workspace = true quarto-error-reporting = { workspace = true, features = ["json"] }