diff --git a/AGENTS.md b/AGENTS.md index 06d63c4..2c11d77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,7 +65,7 @@ Generates a Svelte Playground link with the provided code. After completing the - `crates/local-core` owns canonical parsing, deterministic serialization, filesystem safety, atomic writes, indexing, watching, reconciliation, credentials, and domain operations. - `crates/mcp-server` adapts MCP tools to local-core services and must not duplicate business logic. - `src-tauri` owns the desktop process and typed IPC/event bridge. -- A future `apps/server` may use Bun, TypeScript, and Hono. It is outside the Initial MVP and must not become a dependency of local features. +- `apps/server` contains the experimental encrypted sync service built with Bun, TypeScript, and Hono. It must not become a dependency of local features. It serves the browser build of `apps/app`, which includes the account pages and encrypted share viewer. ### Workspace schema ownership diff --git a/README.md b/README.md index 009ae7c..e049ff5 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,137 @@ # Noura -Noura is the open workspace for humans and AI. The Local Alpha stores durable workspace state in ordinary Markdown and binary files. A local SQLite database supplies its disposable index, metadata cache, and full-text search. +The open workspace for humans and AI. -## Status +Noura is an open-source, local-first desktop workspace where everything is a plugin. Combine notes, tasks, projects, and AI chat in one workspace. Keep your work in ordinary files you can edit, back up, and use without Noura. -The repository contains the desktop Local Alpha with: +[![Status: Local Alpha](https://img.shields.io/badge/status-Local%20Alpha-8b5cf6)](#try-noura) [![Continuous integration](https://github.com/lobbystack/noura/actions/workflows/ci.yml/badge.svg)](https://github.com/lobbystack/noura/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) -- File-backed notes, tasks, and projects -- Month Calendar projection -- External-edit reconciliation and conflict review -- Model Context Protocol (MCP) mutations -- Full-text search -- Index recovery from canonical workspace files +[Try Noura](#try-noura) · [Documentation](#documentation) · [Contribute](#contributing) -The repository also contains: +## A workspace built from plugins -- `apps/server`: an experimental encrypted sync service serving the unified `apps/app` browser build, not a production release. Account, device approval, invitation, and encrypted share pages work independently of native workspace initialization. Browser workspaces run in the hosted build and remain experimental. See [`apps/server/README.md`](apps/server/README.md). -- `apps/website`: the marketing site. +Notes, tasks, projects, calendar, folders, and AI are first-party plugins. Choose the combination you need for each workspace in **Settings**. -See [`docs/architecture/`](docs/architecture/) for design notes and [`docs/workspace-format/`](docs/workspace-format/) for the workspace format. +Start with notes for writing, then add tasks and projects to organize the work around them. Turn plugins on or off without deleting their files. Plugins use [shared capabilities](docs/architecture/plugin-runtime.md#capabilities) to work with your files and contribute commands or AI context. -## Development +## Work in one workspace -Install Bun and Rust 1.91 or newer. The browser workspace build also requires the Rust Wasm target and the wasm-bindgen CLI version that matches the Rust dependency: +Write project notes, track tasks, and give your AI assistant context from the same workspace: + +- **Notes**: write and edit Markdown +- **Tasks and projects**: track priorities, due dates, and progress on project boards +- **Calendar**: view scheduled work by month, week, or day +- **Search**: find content across your workspace with full-text search +- **AI chat**: connect your provider, choose workspace context, and approve tool actions +- **External AI tools**: read and update notes and tasks through [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) + +## Keep control of your work + +Use Noura alongside your existing tools: + +- **Open files**: keep notes, tasks, projects, and chat history as Markdown with structured metadata +- **Local use**: read and edit your workspace offline, without a Noura account or hosted service +- **External editing**: edit, move, and rename files with other tools; review conflicts when changes overlap +- **AI permissions**: choose your provider and authorize sending workspace content before an in-app AI request +- **Credentials**: Noura stores provider credentials in your operating system’s credential store + +## Try Noura + +Run the desktop app from source. Install these prerequisites: + +- [Bun](https://bun.sh/docs/installation) 1.3.14 +- [Rust](https://www.rust-lang.org/tools/install) 1.91 or newer +- [Tauri 2 platform prerequisites](https://v2.tauri.app/start/prerequisites/) for your operating system + +```sh +git clone https://github.com/lobbystack/noura.git +cd noura +bun install +bun run tauri dev +``` + +Create a workspace, add a note, and open the note’s Markdown file in your editor. + +## Develop Noura + +The browser workspace build also requires the Rust Wasm target and the wasm-bindgen CLI version that matches the Rust dependency: ```sh rustup target add wasm32-unknown-unknown cargo install wasm-bindgen-cli --version 0.2.127 --locked ``` -Then run: +Run the development checks from the repository root: ```sh -bun install bun run check bun run test +bun run build +bun run format:check ``` -Run the desktop development host with `bun run tauri dev` after installing the Tauri platform prerequisites. +Follow the [desktop verification guide](docs/testing/local-alpha-acceptance.md) for the full checks and workspace workflows. -Run the marketing website from the repository root: +To work on the marketing website, start its development server: ```sh bun run dev:website ``` -The marketing site runs at `http://127.0.0.1:5174`. Its embedded product demo uses the application development server at `http://127.0.0.1:5173` when that server is running. +Open `http://127.0.0.1:5174` in your browser. + +## How Noura stores your workspace + +Your workspace is a folder with a [manifest](docs/workspace-format/v1.md#manifest) at `.noura/workspace.yaml`. Notes, tasks, projects, and chats use Markdown with structured metadata in [frontmatter](docs/workspace-format/v1.md#managed-markdown). You can move or rename a file without changing its stable identifier. + +Noura commits workspace files to disk before reporting a successful change. You can rebuild its SQLite search index and metadata cache from those files. The desktop app and MCP server use the same Rust services: + +```mermaid +flowchart LR + desktop["Noura desktop"] --> core["Local core"] + tools["MCP tools"] --> core + core <--> files["Workspace files"] + core --> index["Rebuildable SQLite index"] +``` + +Read the [workspace format](docs/workspace-format/v1.md) for file layouts and the [local-core architecture](docs/architecture/local-core.md) for write and recovery behavior. + +## Repository structure + +Start with the directory for the part you want to work on: + +| Directory | Purpose | +| --- | --- | +| [`apps/app`](apps/app) | SvelteKit and Svelte 5 interface for the desktop app, account pages, share viewer, and experimental browser workspace | +| [`apps/website`](apps/website) | Marketing website | +| [`apps/server`](apps/server) | Experimental sync service that hosts the browser build of `apps/app` | +| [`packages`](packages) | Typed workspace client, shared types, editor, AI runtime, and plugin contracts | +| [`crates/local-core`](crates/local-core) | Rust file operations, validation, indexing, watching, and credentials | +| [`crates/mcp-server`](crates/mcp-server) | MCP adapter over local-core services | +| [`plugins`](plugins) | First-party workspace modules | +| [`src-tauri`](src-tauri) | Tauri 2 desktop shell and native bridge | + +## Documentation + +Start with the [documentation guide](docs/README.md), or use these references for implementation details: + +- [Workspace format](docs/workspace-format/v1.md) +- [Local-core architecture](docs/architecture/local-core.md) +- [AI runtime and permissions](docs/architecture/ai-runtime.md) +- [Plugin capabilities](docs/architecture/plugin-runtime.md) +- [Desktop verification guide](docs/testing/local-alpha-acceptance.md) +- [Server setup](apps/server/README.md) + +## Contributing + +Read the [contributor conventions](AGENTS.md) before changing code. Keep pull requests focused, include tests for behavior changes, and run the development checks. + +For workspace-format changes, update the shared [conformance fixtures](docs/workspace-format/fixtures/) so Rust and TypeScript accept the same files. -## Storage contract +## Support and security -Workspace files are canonical. Paths identify current locations, while frontmatter IDs identify managed objects. Noura can rebuild a deleted local index without losing durable workspace data. See the [workspace format](docs/workspace-format/v1.md) for details. +Use [GitHub issues](https://github.com/lobbystack/noura/issues) for questions, bug reports, and feature requests. Follow the [security policy](SECURITY.md) to report a vulnerability. -## Security +## License -Report vulnerabilities privately. See [SECURITY.md](SECURITY.md). +Noura uses the [MIT license](LICENSE). See [third-party notices](THIRD_PARTY_NOTICES.md) for dependency attributions. diff --git a/SECURITY.md b/SECURITY.md index add5dde..4e66ba7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ Report suspected vulnerabilities privately. Do not open a public issue. ## Report a vulnerability -Use GitHub's private vulnerability reporting: [Report a vulnerability](https://github.com/lobbystack/noura/security/advisories/new). +Use [GitHub’s private vulnerability reporting](https://github.com/lobbystack/noura/security/advisories/new). If you cannot use GitHub, email `hello@lobbystack.com`. Include a description, reproduction steps, and the affected version or commit. diff --git a/apps/server/OPERATIONS.md b/apps/server/OPERATIONS.md index 91e0d77..86b96ce 100644 --- a/apps/server/OPERATIONS.md +++ b/apps/server/OPERATIONS.md @@ -1,18 +1,40 @@ # Operating the development server -The Docker image contains the Bun service, migrations, account pages, public viewer, and a generated dependency-notice file. Compose starts it as the unprivileged `bun` user. Railway mounts volumes as root, so on Railway set `RAILWAY_RUN_UID=0`: the image entrypoint then starts as root only long enough to take ownership of the mounted `BLOB_ROOT`, and drops to `bun` before the service process starts. The long-running server never runs as root. Put HTTPS termination in front of it and configure `PUBLIC_ORIGIN` to the exact external origin. Allowlisted email, mail delivery (`SMTP_URL` or `RESEND_API_KEY`), and passkey origin configuration must agree with that origin. Set `TRUSTED_IP_HEADER` to the header your reverse proxy overwrites with the client address; authentication rate limiting keys on it. +Deploy, back up, and validate the Noura sync service with this runbook. The service remains an experimental collaboration implementation. Do not treat a configured GitHub Actions workflow or a local probe as release approval. Developers should use the [server README](README.md) for local setup and API contracts. + +## Prepare a deployment + +The image runs as the unprivileged `bun` user. Run it behind HTTPS termination and set `PUBLIC_ORIGIN` to that exact external origin. Passkey configuration, allowlisted email, and mail delivery must use the same origin. The supplied Compose configuration binds the service to `127.0.0.1:1900`; run Caddy or another TLS proxy on the host. + +Railway mounts volumes as root. On Railway, set `RAILWAY_RUN_UID=0`: the image entrypoint starts as root only to take ownership of the mounted `BLOB_ROOT`, then drops to `bun` before the service starts. The long-running server never runs as root. + +Set these values in an operator secret store: + +- `DATABASE_URL` +- `AUTH_SECRET`, with at least 32 random characters +- `MAIL_FROM` +- One mail delivery setting: `SMTP_URL` or `RESEND_API_KEY` +- `POSTGRES_PASSWORD` when using Compose + +Set `ALLOWED_EMAILS` for every account that may sign in. An empty value admits no accounts. Set `TRUSTED_IP_HEADER` only to a header that the reverse proxy overwrites with the client address. Authentication rate limiting uses that header when configured. + +Keep `AUTH_SECRET`, mail credentials, and database credentials out of the repository. PostgreSQL stores session hashes, encrypted workspace operations, and account and authorization metadata. Restrict database access and protect backups. The service does not hold workspace content keys, so a database restore cannot recover lost device keys or recovery material. ## Startup and health -Run `bun migrate.js` from the image before starting `bun main.js`. The supplied Compose file separates migration from the service and waits for PostgreSQL. `/health` checks process liveness; `/ready` also checks database/schema readiness. Neither endpoint establishes that mail delivery or passkey registration works. +Run migrations before starting the service. The supplied Compose file runs `bun migrate.js` after PostgreSQL passes its health check, then starts `bun main.js`. + +`/health` checks process liveness. `/ready` also checks database and schema readiness. Neither endpoint checks mail delivery or passkey registration. -Keep `AUTH_SECRET`, mail delivery credentials, and database credentials in the operator's secret store. The database stores session hashes and encrypted workspace operations. It still contains account and authorization metadata, so restrict database access and protect backups. The server never possesses workspace content keys; a database restore cannot recover lost users' device keys and recovery material. +The server accepts a non-loopback `PUBLIC_ORIGIN` only over HTTPS. Its optional S3 endpoint also requires HTTPS except on loopback. The repository does not provision a domain or a managed production instance. ## Backup and restore -The implementation stores encrypted operations, public snapshots, key envelopes, policies, blob metadata, and authentication records in PostgreSQL. Attachment ciphertext lives in `BLOB_ROOT`; completed objects may also live in configured S3-compatible storage. A backup must capture PostgreSQL and local staging at one offline boundary and preserve every referenced S3 object. Preserve the matching server image and operator configuration separately, including `AUTH_SECRET`. Do not commit backups or secrets to Git. +Capture the database and local blob storage at one offline boundary. Stop every server writer, wait for requests and storage finalization to finish, then capture PostgreSQL and `BLOB_ROOT` together. Do not combine independently timed database and blob backups while writes are active. -Use PostgreSQL service definitions and a protected password file instead of placing credentials directly in commands. With `noura` and `noura-restore` service definitions configured by the operator: +PostgreSQL contains encrypted operations, public snapshots, key envelopes, policies, blob metadata, and authentication records. `BLOB_ROOT` contains attachment ciphertext and defaults to `./data/blobs`; Compose mounts it at `/data/blobs`. Preserve referenced S3 objects and the bucket configuration when S3 storage is enabled. Keep the matching server image, operator configuration, and authentication secrets in a separate protected backup. + +Use PostgreSQL service definitions and a protected password file instead of credentials in commands. With `noura` and `noura-restore` service definitions: ```sh umask 077 @@ -20,26 +42,71 @@ pg_dump --dbname=service=noura --format=custom --no-owner --no-acl --file=noura. pg_restore --exit-on-error --no-owner --no-acl --dbname=service=noura-restore noura.dump ``` -Restore into an empty, isolated database, never over the running service. Start the matching image against it with outbound email disabled at the network boundary. Compare table counts and deterministic row hashes with the backup's recorded inventory. Then exercise readiness, a prearranged test account, an authorized encrypted pull, and a revoked token/link. Only switch production traffic after those checks succeed. Keep the old database isolated until the operator's rollback window closes. +Restore into an empty, isolated database, never over the running service. Start the matching image with outbound email blocked at the network boundary. Compare table counts and deterministic row hashes with the backup inventory. Then check readiness, a prearranged test account, an authorized encrypted pull, and a revoked token or public link. Switch production traffic only after these checks succeed. Keep the old database isolated until the rollback window closes. + +## Attachment storage and retention + +Keep `BLOB_ROOT` on durable storage. PostgreSQL holds quota reservations, TUS metadata, and completion records; attachment bytes remain separate ciphertext files. There is no storage garbage collection yet, so monitor database and blob growth. + +Set `S3_BUCKET`, `S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY` to store completed ciphertext with Bun’s S3 client. `S3_REGION` defaults to `us-east-1`. Use the optional `S3_ENDPOINT` for a custom endpoint; HTTPS is required outside loopback. -A local PostgreSQL 16 rehearsal on 2026-09-05 restored the complete integration test database into a new temporary database and compared every public table's row count and sorted JSON row hash. It passed, and the rehearsal removed the temporary database. This verifies the current database-only backup mechanics; it is not a production disaster-recovery rehearsal, offsite retention policy, or measured recovery-time guarantee. +Local staging remains durable after completion, so back up the bucket and local staging with PostgreSQL. Validate provider versioning, outage behavior, retention, bucket policies, and recovery time for the selected storage service. ## Limits and failure behavior -There is no history pruning or snapshot compaction yet. Monitor database growth. The default workspace ciphertext quota is 1 GiB. The service limits requests to 2 MiB, individual ciphertext envelopes to 1 MiB, and resumable attachment ciphertext to 1 GiB. Rate limits apply per account; this is not a substitute for perimeter connection limits or a measured capacity plan. +Monitor capacity against these current limits: + +- 1 GiB ciphertext quota per workspace +- 2 MiB encoded request body +- 1 MiB ciphertext per operation +- 1 GiB resumable attachment ciphertext +- 120 requests per account per minute + +No history pruning or snapshot compaction runs yet. Account rate limits do not replace perimeter connection limits or a measured capacity plan. + +Native clients retain ordered encrypted outboxes during outages. Pull cursors advance only after canonical file application or durable conflict preservation. Missing content keys and untrusted senders stop a pull without advancing its cursor. Local files remain usable while the service is unavailable. -For a repeatable local transport probe, run from the repository root: +Device revocation blocks future server access. Removing access requires a new object-key epoch before later updates; clients also need the matching rotation and approval workflows. Revocation cannot erase plaintext or keys that a recipient obtained earlier. Public-link revocation blocks later server fetches, but recipients can retain decrypted snapshots. + +## File-relay load probe + +Run this transport probe from the repository root against a disposable database: ```sh NOURA_TEST_DATABASE_URL=postgres://user@localhost/noura_test bun apps/server/scripts/load-probe.ts ``` -It creates unique test fixtures and measures delivery of one signed opaque operation to 100 simultaneous HTTP readers. A local PostgreSQL 16 run on 2026-09-05 measured 42 ms p50, 54 ms p95, and 56 ms maximum from push start through response parsing, with zero remaining subscriptions. These are local transport measurements, not WAN performance, a soak test, or the planned 20-editor collaboration acceptance test. The probe's payload is synthetic framing data; native integration tests separately exercise authenticated encryption and file application. +The probe creates unique fixtures, opens 100 simultaneous HTTP pull clients, and submits one signed opaque operation from each of 20 concurrent writers. Each client must receive all 20 operations once. The probe fails when p95 delivery is at least 1,000 ms or subscriptions remain after cleanup. + +## Realtime collaboration load and soak probe + +Run the realtime probe against a disposable PostgreSQL database: + +```sh +NOURA_TEST_DATABASE_URL=postgres://user@localhost/noura_test bun run --cwd apps/server test:collaboration-load +NOURA_TEST_DATABASE_URL=postgres://user@localhost/noura_test NOURA_SOAK_SECONDS=3600 bun run --cwd apps/server test:collaboration-load +``` + +The default run connects 100 authenticated WebSockets. Twenty writers each submit one version-two text operation with 100 ms simulated round-trip time (RTT). Every client must retrieve every operation through the authoritative HTTP pull. The default p95 budget is below 1,000 ms. The regular server workflow sets `NOURA_LOAD_P95_BUDGET_MS=3000` for its shared runner; its configuration does not establish a passing CI outcome. + +The probe fails for p95 at or above the configured budget, duplicate or missing delivery, incomplete fanout, subscription leakage, or RSS growth above 256 MiB after connection warmup. A recovery pull runs after five seconds for missed notifications. The soak run repeats the burst once per second for one hour while retaining a fixed-size latency histogram. + +Bounded overrides include `NOURA_LOAD_CLIENTS`, `NOURA_LOAD_WRITERS`, `NOURA_LOAD_RTT_MS`, `NOURA_LOAD_INTERVAL_MS`, `NOURA_MAX_RSS_GROWTH_MIB`, and `NOURA_LOAD_P95_BUDGET_MS`. Set `NOURA_LOAD_FORCE_RECONNECT_BATCH=1` to close one socket after the first committed burst and verify an authoritative pull before reconnect. Set `NOURA_LOAD_FORCE_RECOVERY_BATCH=1` to suppress one client's first-batch notification and verify the five-second recovery pull. The regular server workflow configures both faults. The scheduled soak workflow configures 100 clients, 20 writers, 100 ms RTT, and a one-hour duration; configuration is not execution evidence. + +These probes exercise WebSocket invalidation and signed operation pulls. They do not replace multi-process native CRDT application, cross-platform desktop acceptance, or the 20-editor latency measurement required for release. + +## Offline restore rehearsal + +Run `bun run --cwd apps/server test:restore` only with a disposable `NOURA_TEST_DATABASE_URL` that permits `CREATE DATABASE`. The rehearsal creates two unique databases and never restores over an existing database. Install PostgreSQL 16 `pg_dump` and `pg_restore` on `PATH`, or set `NOURA_TEST_PG_BIN` to their directory. + +The rehearsal waits for synthetic requests to finish, closes its only writer, dumps PostgreSQL, and copies the local blob directory. It restores both to fresh locations and verifies sessions, signed operations, cursors, quotas, complete ciphertext, a partial-upload continuation, staged and committed transitions, checkpoints, generations, and blob manifests. It removes the original blob directory before verification, then removes temporary databases and files. + +Set `NOURA_TEST_S3_ENDPOINT`, `NOURA_TEST_S3_BUCKET`, `NOURA_TEST_S3_ACCESS_KEY`, and `NOURA_TEST_S3_SECRET_KEY` to include an S3-compatible test server. This mode backs up a completed synthetic object, deletes it from the test bucket, restores it, removes its completed local cache, and checks the read. It uses unique object keys and removes only its own objects. It does not create a bucket or test provider versioning, regional outages, bucket policies, or production recovery time. -Native clients retain ordered encrypted outboxes through outages. Pull cursors advance after canonical file application or durable conflict preservation. Missing content keys and untrusted senders stop a pull without advancing its cursor. Local files remain usable when the service is unavailable. +The repository configures this rehearsal in the regular server workflow. Verify the workflow's current GitHub result before relying on it as CI evidence. -Device revocation prevents future server access. Removing access requires a new object-key epoch before further updates; clients still need the corresponding rotation and approval workflows. Revocation cannot erase plaintext or keys a recipient previously obtained. Public-link revocation prevents further server fetches; a recipient can retain an already decrypted snapshot. +## Public service scope and security caveats -## Release gates still open +This service stores and routes signed encrypted records. Native clients retain content keys and canonical workspace files. The service remains experimental, and standard startup disables collaboration capabilities. Do not represent it as a production Noura collaboration service. -Invitation and permission-management UI, recovery re-invitation and rotation, collaborative text editing and presence, compaction, attachment garbage collection, and desktop public publishing remain unfinished. Sustained soak measurements, multi-platform desktop UI runs, real SMTP/passkey deployment checks, and external security review are also outstanding. This image is a development build. +Collaboration notifications and transient presence do not replace durable HTTP pull and acknowledgment. Follow the protocol contracts and validation procedures before enabling experimental routes. diff --git a/apps/server/README.md b/apps/server/README.md index 62c1c96..9fb48d7 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -1,28 +1,14 @@ # Noura sync service (development) -An original Bun/Hono/PostgreSQL service for signed encrypted operations. This is the server foundation and an experimental collaboration implementation, **not a production Noura collaboration release**. Production startup deliberately withholds collaboration capabilities until the remaining release gates below pass. - -Once enabled, rollout admission and protocol continuity are separate. Setting `collaborationRollout: false` while retaining `checkpointTransitions: true` blocks new workspace capabilities while continuing to serve existing checkpoints, transitions, live-text generations, and capability-bound object activations. - -## Implemented - -- Better Auth passkeys and emailed magic links, with an operator email allowlist. -- Account-authenticated, single-use device challenges and Ed25519 possession proofs. -- Seven-day opaque device sessions, stored as hashes; renewal invalidates the previous session, and revoked device IDs cannot enroll again. -- Workspace and object creation, device listing/revocation, and workspace listing. -- Atomic signed operation batches, durable retry IDs, increasing decimal-string cursors, payload quotas, per-account rate limits, and object-filtered downloads. -- Bounded notification-driven pulls using one shared PostgreSQL listener, with authentication and access rechecked after waiting. -- Real PostgreSQL integration tests, including account login with captured mail. -- Browser-approved native device sign-in with OS-held credentials, a desktop Settings account panel, and passkey/magic-link browser account pages. -- Signed access-policy revisions, member/object permissions, device-wrapped content keys, epoch rotation enforcement, chained policy-history verification, per-revision writer authorization, and permission-filtered key retrieval. -- Revocable encrypted public snapshots and a read-only browser viewer. Decryption keys and the pinned signer travel in the URL fragment, never the HTTP request. -- Native AES-GCM/Ed25519 file transport, age key wrapping, encrypted key-envelope persistence, a file-backed crash-replay journal, and conflict preservation. -- Server-side browser device support: enrollment of `x25519:` recipients verified with the `noura.device.enroll.web` proof, storage of signed `noura.sync.key.web` envelopes, browser envelope verification in key upload and signed access policies, and construction-aware key delivery. The browser client library (`packages/browser-sync`) handles device custody, enrollment, key delivery, and operation transport. The hosted app does not call it yet, and no test has run it against this service end to end. -- Native background capture and restart, pause/resume, owner replica joining, verified device approval, encrypted key backup before upload, and explicit local/remote resolution of same-path conflicts without overwriting later edits. -- Owner-signed workspace capabilities, automatic invitation activation, atomic checkpoint/key rotations, isolated fresh-recipient history, and resumable transition-bound checkpoint blobs. -- Writer-authorized object activation plus native create, update, move, delete, external-change, MCP, managed metadata/body, and plain-text collaboration paths. -- Yjs/Yrs text generations with acknowledged baselines, durable drafts, restart recovery, generation rebase/review, and an isolated bounded native decoder worker. -- Authenticated realtime notifications and encrypted transient presence, with durable HTTP pull/acknowledgment remaining authoritative. +Develop signed, encrypted sync operations with this Bun, Hono, and PostgreSQL service. It is an experimental collaboration implementation, not a production Noura collaboration release. Production startup does not enable collaboration capabilities. See [operating procedures](OPERATIONS.md) for deployment requirements, backups, and validation procedures. + +Once enabled, rollout admission and protocol continuity remain separate. Setting `collaborationRollout: false` with `checkpointTransitions: true` blocks new workspace capabilities while the service continues existing checkpoints, transitions, live-text generations, and capability-bound object activations. + +## Experimental service capabilities + +The experimental service supports approved-device authentication, signed encrypted operation exchange, signed access policies, device-wrapped content keys, and revocable encrypted public snapshots. Native clients retain canonical file application, durable journals, conflict preservation, and local key material. Experimental invitation, checkpoint, text-generation, notification, and presence features retain durable HTTP pull and acknowledgment as their authoritative transport. + +The service also supports browser devices. It verifies `x25519:` recipient enrollment and browser key envelopes, and delivers keys by envelope construction. The browser client library, `packages/browser-sync`, handles device custody, enrollment, key delivery, and operation transport. The hosted app does not call it yet, and no test runs it end to end against this service. ## Run locally @@ -36,33 +22,50 @@ bun run start To include the account pages and public viewer, run `bun run build:release`, then `bun dist/main.js`. Docker packages this combined release automatically. -`apps/app` builds the browser UI. Its `(account)` route group contains `/account`, `/account/device`, `/invite/[token]`, and `/share/[token]`, outside the `(workspace)` layout and its native initialization. Browser workspace routes run the hosted notes, tasks, and projects client over browser storage and encrypted sync; they remain experimental. +`apps/app` builds the browser UI. Its `(account)` route group contains `/account`, `/account/device`, `/invite/[token]`, and `/share/[token]`, outside the `(workspace)` layout and its native initialization. Browser workspace routes run the hosted notes, tasks, and projects client over browser storage and encrypted sync. They remain experimental. -`build:release` creates `apps/app/build-hosted`, verifies CSP hashes and the account routes' static import boundary, generates bundled dependency notices, and copies the app to `dist/public`. Native builds continue to use `apps/app/build` and Tauri's CSP. Do not substitute a native build for the hosted build: the hosted build preserves the restrictive account/share CSP. +`build:release` creates `apps/app/build-hosted`, verifies CSP hashes and the account routes’ static import boundary, generates bundled dependency notices, and copies the app to `dist/public`. Native builds use `apps/app/build` and Tauri’s CSP. Don’t substitute a native build for the hosted build: only the hosted build keeps the restrictive account and share CSP. -The server serves only explicit SPA destinations and asset prefixes. Missing API endpoints, missing assets, unknown paths, and non-GET page requests do not fall back to HTML. Auth callback paths and the server's `PUBLIC_ORIGIN` are unchanged. For frontend development, `bun run --cwd ../app dev` proxies `/api`, `/public`, and `/v1` to loopback port 1900. Configure the server's `PUBLIC_ORIGIN` to match the frontend origin when using that proxy; test secure cookies and passkeys on the same-origin combined release before deployment. +The server serves only explicit SPA destinations and asset prefixes. Missing API endpoints, missing assets, unknown paths, and non-GET page requests don’t fall back to HTML. For frontend development, `bun run --cwd ../app dev` proxies `/api`, `/public`, and `/v1` to loopback port 1900. Set the server’s `PUBLIC_ORIGIN` to the frontend origin when you use that proxy. Test secure cookies and passkeys on the same-origin release build before deployment. -Use `/health` for liveness and `/ready` for database/schema readiness. Production requires an HTTPS `PUBLIC_ORIGIN`; Noura allows plain HTTP only for loopback. +Use `/health` for liveness and `/ready` for database/schema readiness. Production requires an HTTPS `PUBLIC_ORIGIN`; plain HTTP is allowed only for loopback. -`docker compose --env-file .env -f compose.yaml up --build` starts PostgreSQL, runs migrations, then starts the service. Also set `POSTGRES_PASSWORD` for Compose, using a URL-safe random value. The application listens only on host loopback; use the example Caddy configuration for external TLS termination. This code provisions neither a domain nor a managed production instance. +`docker compose --env-file .env -f compose.yaml up --build` starts PostgreSQL, runs migrations, then starts the service. Also set `POSTGRES_PASSWORD` for Compose, using a URL-safe random value. The application listens only on host loopback; use the example Caddy configuration for external TLS termination. Neither a domain nor a managed production instance is provisioned by this code. ## API Account routes live under `/api/auth/*` and use Better Auth cookies. Create a device challenge with `POST /v1/device-challenges` using the authenticated cookie and same-origin `Origin` header. The response contains `challenge`, `accountId`, and a five-minute lifetime. -Sign the UTF-8 JSON tuple below with the device's Ed25519 private key: +Sign the canonical UTF-8 bytes of this JSON tuple with the device's Ed25519 private key. Serialize the array with `JSON.stringify` and no added whitespace or fields: ```text -["noura.device.enroll", serverOrigin, accountId, deviceId, publicKeyBase64, challenge] +["noura.device.enroll",serverOrigin,accountId,deviceId,publicKeyBase64,challenge] ``` Send `{deviceId, publicKey, challenge, proof}` to `POST /v1/devices` using the same cookie and origin. Public keys are 32 raw bytes in canonical base64; proof signatures are 64 bytes in canonical base64. The response supplies a bearer token. A fresh challenge/proof renews an existing non-revoked device session. This authenticates a device; it does **not** distribute content keys. -Native clients use Better Auth's device-authorization flow and enroll an age recipient with `{encryptionRecipient}` included. Their v2 proof signs `["noura.device.enroll.v2",serverOrigin,accountId,deviceId,publicKeyBase64,encryptionRecipient,challenge]`. Native code exchanges the temporary account bearer session for a Noura device session and then signs out. +Native clients use Better Auth's device-authorization flow and include `encryptionRecipient`. Their v2 proof signs these canonical UTF-8 bytes: -Browser devices enroll an `x25519:` recipient (the prefix plus standard padded base64 of a 32-byte X25519 public key). Their proof signs `["noura.device.enroll.web",1,serverOrigin,accountId,deviceId,publicKeyBase64,encryptionRecipient,challenge]`. An unusable recipient returns `sync.invalid_recipient`; a bad proof returns `sync.invalid_signature`. Noura stores the recipient string unchanged, and the string grants no content key. +```text +["noura.device.enroll.v2",serverOrigin,accountId,deviceId,publicKeyBase64,encryptionRecipient,challenge] +``` + +The temporary account bearer session is exchanged for a Noura device session inside native code and then signed out. + +Browser devices enroll an `x25519:` recipient: the prefix plus standard padded base64 of a 32-byte X25519 public key. Their proof signs these canonical UTF-8 bytes: + +```text +["noura.device.enroll.web",1,serverOrigin,accountId,deviceId,publicKeyBase64,encryptionRecipient,challenge] +``` -Key upload accepts an optional envelope `construction`. An absent discriminator or `"age"` is the existing native seven-field record signed over `["noura.sync.key",1,workspaceId,objectId,epoch,signingDevice,deviceId,wrappedKey]`. `"web"` adds `recipientPublicKey`, `ephemeralPublicKey`, `salt`, and `nonce`, signed over the `noura.sync.key.web` version 1 tuple, and must wrap to the recipient device's enrolled `x25519:` key. `noura_key_envelopes` stores `construction` (default `'age'`) plus the four nullable browser columns, and key delivery and `access-state` return them for every envelope. +An unusable recipient returns `sync.invalid_recipient`, and a bad proof returns `sync.invalid_signature`. The service stores the recipient string unchanged; the string grants no content key. + +Key upload accepts an optional envelope `construction`: + +- **Absent or `"age"`**: the native seven-field record, signed over `["noura.sync.key",1,workspaceId,objectId,epoch,signingDevice,deviceId,wrappedKey]` +- **`"web"`**: adds `recipientPublicKey`, `ephemeralPublicKey`, `salt`, and `nonce`, signed over the `noura.sync.key.web` version 1 tuple, and must wrap to the recipient device’s enrolled `x25519:` key + +`noura_key_envelopes` stores `construction` (default `'age'`) and the four nullable browser columns. Key delivery and `access-state` return them for every envelope. All remaining `/v1` routes require `Authorization: Bearer `: @@ -71,6 +74,8 @@ All remaining `/v1` routes require `Authorization: Bearer `: | GET | `/v1/devices` | List your devices | | DELETE | `/v1/devices/:id` | Revoke your device and its sessions | | GET/POST | `/v1/workspaces` | List/create (`{id}`) workspaces | +| POST/GET | `/v1/workspaces/:id/invitations` | Create/list workspace invitations | +| DELETE | `/v1/workspaces/:id/invitations/:id` | Revoke a pending workspace invitation | | POST | `/v1/workspaces/:id/objects` | Create an opaque object (`{id}`) | | POST | `/v1/workspaces/:id/operations` | Upload `{operations: [...]}` | | GET | `/v1/workspaces/:id/operations?after=0` | Fetch an authorized page | @@ -83,76 +88,77 @@ All remaining `/v1` routes require `Authorization: Bearer `: `GET /public/:token` returns an active encrypted public snapshot anonymously. `/share/:token` is its browser viewer, `/account` handles sign-in, and `/account/device` explicitly approves or denies desktop device codes. +## Invitation and access semantics + +Workspace owners create invitations for the `admin`, `editor`, or `viewer` role. An invitation expires after seven days, and a workspace can have at most 100 active invitations. A link can be accepted by one account and can be revoked while pending. + +Accepting an invitation records the prospective account. It does not grant membership or distribute content keys. Membership begins only after the owner signs an access policy and wraps object keys for approved recipient devices. The native coordinator rotates each checkpoint-enrolled object when effective recipient devices change. + The native file and key formats are documented in [`sync-v1.md`](../../docs/workspace-format/sync-v1.md). -The operation contract lives in `packages/shared/src/sync.ts`. Sign the fixed tuple defined in `src/protocol.ts`. Operation payloads are client-encrypted AES-256-GCM ciphertext including the tag, with a fresh 12-byte nonce. The server validates framing and signatures; it cannot prove a malicious client encrypted its submitted bytes. Clients must validate/decrypt incoming envelopes. +The operation contract lives in `packages/shared/src/sync.ts`. Sign the fixed tuple defined in `src/protocol.ts`: serialize the array with `JSON.stringify`, encode it as UTF-8, and sign those bytes. Omit `generation` and `kind` for version 1 operations. + +```text +["noura.sync.operation",version,workspaceId,objectId,deviceId,operationId,epoch,policyRevision,nonceBase64,ciphertextBase64,generation,kind] +``` + +Operation payloads are client-encrypted AES-256-GCM ciphertext including the tag, with a fresh 12-byte nonce. The server validates framing and signatures; it cannot prove a malicious client encrypted submitted bytes. Clients must validate and decrypt incoming envelopes. -Limits: 100 operations per batch, 1 MiB ciphertext per operation, 2 MiB encoded request body, 1 GiB ciphertext quota per workspace, and 120 requests per account per minute. The first limit reached applies. The service does not prune history automatically. Matching retries return the original sequence; changed bytes under an existing operation ID return a conflict. A failed operation rolls back the whole batch. +Limits: 100 operations per batch, 1 MiB ciphertext per operation, 2 MiB encoded request body, 1 GiB ciphertext quota per workspace, and 120 requests per account per minute. The first limit reached applies. No history is automatically pruned. Matching retries return the original sequence; changed bytes under an existing operation ID return a conflict. A failed operation rolls back the whole batch. -An idle client can add `wait=25&accessRevision=` to its pull. The server subscribes before reading, waits at most 25 seconds if caught up, and then rechecks the session and permissions. Notifications carry only workspace IDs; all returned operations still pass the normal authorization filter. A process retains at most 1000 concurrent waiters. This long-poll path is an authoritative fallback for the experimental native collaboration client; WebSocket notifications only prompt durable pulls. +An idle client can add `wait=25&accessRevision=` to its pull. The server subscribes before reading, waits at most 25 seconds if caught up, and then rechecks the session and permissions. Notifications carry only workspace IDs; all returned operations still pass the normal authorization filter. At most 1000 concurrent waiters are retained per process. This long-poll path is an authoritative fallback for the experimental native collaboration client; WebSocket notifications only prompt durable pulls. ## Verification +Run the focused TypeScript checks and protocol test from this directory: + ```sh bun run check bun run build bun test src/protocol.test.ts -# Set the dedicated test database URL and all three compiled native probe paths first. +``` + +Run native integration coverage only against a dedicated test database. The tests create unique fixtures and retain them for inspection; they never truncate existing tables. The command fails unless the database URL and every compiled native probe are present. Ordinary repository tests skip database scenarios without that URL; the server CI job requires them. + +Build the native probes from the repository root with `cargo build -p local-core --examples`. Set `NOURA_TEST_DATABASE_URL` and point `NOURA_NATIVE_PROBE`, `NOURA_NATIVE_SIGNIN_PROBE`, and `NOURA_NATIVE_KEYS_PROBE` to the absolute `target/debug/examples/sync_probe`, `signin_probe`, and `keys_probe` paths before running this command: + +```sh bun run test:integration ``` -For native integration coverage, first build `cargo build -p local-core --examples` from the repository root. Set `NOURA_NATIVE_PROBE`, `NOURA_NATIVE_SIGNIN_PROBE`, and `NOURA_NATIVE_KEYS_PROBE` to the absolute `target/debug/examples/sync_probe`, `signin_probe`, and `keys_probe` paths. These run real Rust clients against HTTP and PostgreSQL, including reconnection, conflict preservation, browser approval, device revocation, signed access policies, and native age key distribution that rejects unpinned signers. Coordinator probes also exercise persisted pause/resume, stable moves, deletion, approved peer key distribution, two-way edits, convergence after explicit conflict resolution, and streaming encrypted attachment exchange through resumable uploads. They are not a substitute for tests with three running desktop applications. +These run real Rust clients against HTTP and PostgreSQL, including reconnection, conflict preservation, browser approval, device revocation, signed access policies, and native age key distribution that rejects unpinned signers. Coordinator probes also exercise persisted pause/resume, stable moves, deletion, approved peer key distribution, two-way edits, convergence after explicit conflict resolution, and streaming encrypted attachment exchange through resumable uploads. They are not a substitute for tests with three running desktop applications. `cargo run -p local-core --example relay_authorization_probe --locked` is a separate malicious-relay regression. It gives a synthetic viewer the real object key and its own approved signing identity, then serves a viewer-authored update from a loopback relay. The gate passes only when the client rejects the update as `sync_writer_not_authorized` and leaves the canonical file unchanged. -Use a dedicated test database. The tests create uniquely named fixtures and retain them for inspection; they never truncate existing tables. The integration command fails if the database URL or any compiled native probe is absent. Ordinary repository tests skip database scenarios without that variable; the server CI job requires them. - -To run the attachment and native recovery cases against an S3-compatible test server, also set `NOURA_TEST_S3_ENDPOINT`, `NOURA_TEST_S3_BUCKET`, `NOURA_TEST_S3_ACCESS_KEY`, and `NOURA_TEST_S3_SECRET_KEY`. These are separate from production storage variables. The tests use unique workspace/object IDs. A local MinIO run has verified resumed uploads, S3 range reads without a local cache, native attachment exchange, and recovery. This does not certify a production bucket's backup or retention configuration. +To run the attachment and native recovery cases against an S3-compatible test server, also set `NOURA_TEST_S3_ENDPOINT`, `NOURA_TEST_S3_BUCKET`, `NOURA_TEST_S3_ACCESS_KEY`, and `NOURA_TEST_S3_SECRET_KEY`. These are separate from production storage variables. The tests use unique workspace/object IDs. Validate a production bucket's backup and retention configuration separately. ## File-relay load probe -With `NOURA_TEST_DATABASE_URL` set, run `bun scripts/load-probe.ts` for 100 HTTP clients and 20 simultaneous writers. Every client must receive all 20 signed operations once, with p95 delivery below one second and no remaining subscriptions. A local macOS/PostgreSQL run delivered all 2,000 operations with p50 42 ms, p95 54 ms, and maximum 56 ms, measured from each HTTP upload start. This is one synthetic burst on loopback. It does not establish WAN performance, CRDT convergence, collaborative editor latency, or sustained-load behavior. +Run the [file-relay probe procedure](OPERATIONS.md#file-relay-load-probe) against a disposable database. The runbook defines its client and writer counts, acceptance criteria, and scope limits. ## Realtime collaboration load and soak probe -With `NOURA_TEST_DATABASE_URL` set to a disposable PostgreSQL database, run: - -```sh -bun run test:collaboration-load -NOURA_SOAK_SECONDS=3600 bun run test:collaboration-load -``` - -The default run connects 100 authenticated WebSockets, submits one concurrent version-two text operation from each of 20 writers, simulates 100 ms RTT, and requires every client to retrieve every operation through the authoritative HTTP pull. It fails at one-second p95, unexpected or duplicate delivery, incomplete fanout, subscription leakage, or more than 256 MiB RSS growth after connection warmup. A five-second authoritative recovery pull covers a notification that was not processed and the final report exposes the recovery count. The soak form repeats the burst once per second for one hour while keeping latency storage fixed-size. `NOURA_LOAD_CLIENTS`, `NOURA_LOAD_WRITERS`, `NOURA_LOAD_RTT_MS`, `NOURA_LOAD_INTERVAL_MS`, and `NOURA_MAX_RSS_GROWTH_MIB` override the bounded defaults. Setting `NOURA_LOAD_FORCE_RECONNECT_BATCH=1` closes one socket after the first committed burst and verifies recovery through an authoritative pull before reconnect; the ordinary server CI gate enables this fault. It also sets `NOURA_LOAD_FORCE_RECOVERY_BATCH=1` to suppress one different client's first-batch notification and prove the five-second recovery pull closes that gap. - -A local smoke run completed five batches and 10,000 deliveries with 100 clients, 20 writers, 100 ms simulated RTT, p95 202 ms, and 35.6 MiB RSS growth. This exercises the real WebSocket invalidation and signed operation/pull path, but it does not replace multi-process native CRDT application or cross-platform desktop acceptance. - -The corrected one-hour local soak completed 3,596 batches and 7,192,000 exact deliveries with the same 100 clients, 20 writers, and 100 ms simulated RTT. It measured p50 172 ms, p95 204 ms, maximum 1.129 s, and 44.7 MiB RSS growth, with zero reconnects, zero recovery pulls, and no leaked subscriptions. A separate forced-fault run closed one socket and suppressed another client's notification; both recovered through authoritative pulls (`reconnects: 1`, `recoveryPulls: 1`) while p95 remained 211 ms. +Run the [collaboration probe procedure](OPERATIONS.md#realtime-collaboration-load-and-soak-probe) against a disposable database. It documents the p95 default and CI override, fault injections, soak configuration, and scope limits. ## Offline restore rehearsal -`bun run test:restore` creates two uniquely named disposable databases using `NOURA_TEST_DATABASE_URL` (which must permit `CREATE DATABASE`). It needs PostgreSQL 16 `pg_dump` and `pg_restore` on PATH, or their directory in `NOURA_TEST_PG_BIN`. It never restores over an existing database. - -The drill waits for all synthetic requests to complete and closes the only writer before dumping PostgreSQL and copying the local blob directory. It restores both into fresh locations and verifies sessions, signed operations, cursors, quotas, complete ciphertext, continuation of a partial upload, staged and committed transitions, checkpoints, generations, and blob manifests. The drill removes the original blob directory before verification and removes temporary databases and files afterwards. This drill has passed locally and is included in server CI; GitHub CI execution still needs verification. - -To include S3, set the four `NOURA_TEST_S3_*` variables described above. The rehearsal also backs up its completed synthetic object, deletes it from the test bucket, restores it, and removes its completed local cache before checking the read. This mode passed against local MinIO. It uses unique object keys and cleans up only its own objects; it does not recreate the bucket or test provider versioning, regional outages, bucket policies, or production recovery times. +Run the [restore rehearsal](OPERATIONS.md#offline-restore-rehearsal) before changing backup or storage operations. The runbook specifies its offline boundary, test database permissions, S3 mode, and exclusions. -An operator backup needs the same offline boundary: stop every server writer, wait for requests and storage finalization to finish, then capture PostgreSQL and the durable blob directory together. Keep server configuration and authentication secrets in a separate protected backup. With S3 enabled, also preserve the referenced immutable objects and bucket configuration; this local-storage drill does not test all production S3 failure modes. Validate a restore in an isolated deployment before replacing a live service. Never combine independently timed database and blob backups while writes are active. - -## Remaining release requirements +## Attachment storage -Existing-member permission-management UI, folder sharing, attachment conflict resolution and storage garbage collection, snapshot compaction, desktop public publishing, and production backup/restore rehearsal remain unfinished or outside this collaboration milestone. Release still requires the full desktop flow on macOS, Windows/UTM, and Linux; a 20-editor/100 ms RTT latency measurement; the Windows worker capability confinement and crash-durability fault tests; and an independent security review. Backend access and public-link APIs are executable, but do not yet form a complete user-facing sharing workflow. +See [attachment storage and retention](OPERATIONS.md#attachment-storage-and-retention) for durable storage locations, S3 requirements, and backup scope. -The service implements workspace invitation creation, browser acceptance, desktop fingerprint approval, signed membership activation, and pending-invitation revocation. Acceptance does not grant membership until the owner signs the policy and wraps the object keys for approved recipients. The native coordinator rotates every checkpoint-enrolled object when effective recipient devices change. A user-facing flow for arbitrary existing-member role changes/removal and folder sharing is still absent. +## Member roles and replica behavior -## Attachment storage +The native client verifies the signed membership policy before selecting a transfer mode. Workspace members can join as owner, admin, editor, or viewer. Viewer replicas pull remote updates and retain local edits without adding them to the upload queue. Existing queued edits survive a downgrade but are not uploaded. -`BLOB_ROOT` holds durable ciphertext uploads and defaults to `./data/blobs`. The Docker image uses `/data/blobs`, backed by the Compose `blobs` volume. Do not place this directory on ephemeral storage. PostgreSQL stores quota reservations, TUS metadata, and completion records; attachment bytes remain separate files. Backups must include both PostgreSQL and this directory. +## Recipient key distribution -Setting `S3_BUCKET`, `S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY` enables completed ciphertext storage through Bun's S3 client. Optional `S3_ENDPOINT` must use HTTPS except on loopback; `S3_REGION` defaults to `us-east-1`. Compose forwards these settings. Local staging remains durable, and the service currently retains it after completion. A deployment using S3 must back up its bucket as well as PostgreSQL and local staging. S3 transfer and restore have passed against a local MinIO endpoint; provider-specific versioning, outage, and retention behavior remains an operator deployment gate. +`PUT /v1/keys/share` accepts a bounded array of signed recipient envelopes from an existing object writer. Recipients must already have object access and be active devices. This endpoint cannot grant access, change a role, rotate an epoch, or replace an existing recipient ciphertext. It lets editors supply keys for newly created objects without permission-management rights. Native clients still require explicit recipient fingerprint approval before calling it. Signed access policies remain required for membership, grants, and epoch changes. -Workspace members can join as owner, admin, editor, or viewer. The native client verifies the signed membership policy before choosing its transfer mode. Viewer replicas pull remote updates and retain local edits without adding them to the upload queue. Existing queued edits survive a downgrade, but the client does not upload them. +## Desktop recovery kits -`PUT /v1/keys/share` accepts a bounded array of signed recipient envelopes from an existing object writer. Recipients must already have object access and be active devices. This endpoint cannot grant access, change a role, rotate an epoch, or replace an existing recipient ciphertext. It lets editors supply keys for newly created objects without permission-management rights. Native clients still require explicit recipient fingerprint approval before calling it. Signed access policies remain required for membership, grants, and epoch changes. +Recovery kits let a newly signed-in device restore workspace keys without sending secret bytes through IPC. Desktop recovery kits are workspace-bound JSON backups containing an age identity, public trust pins, and encrypted key envelopes. Export/import use native file dialogs; secret bytes never enter IPC. Import into the matching joined workspace restores keys to the newly signed-in device, retrieves authorized later envelopes, and downloads content while leaving synchronization paused. Tests recover files and attachments with a clean credential store that lacks the original device's signing key, and preserve external edits on retry. Re-invitation after membership revocation and the key-rotation UI remain release work. @@ -166,6 +172,6 @@ Set `NOURA_SYNC_ORIGIN` when building the desktop to use another service: NOURA_SYNC_ORIGIN=http://localhost:1909 bun run tauri build --debug --bundles app ``` -Release builds have no implicit service address. Supply `NOURA_SYNC_ORIGIN` for managed sync; custom servers remain available under **Use a self-hosted server…**. The service requires HTTPS except for loopback development addresses. +Release builds have no implicit service address. Supply `NOURA_SYNC_ORIGIN` for managed sync; custom servers remain available under **Use a self-hosted server…**. HTTPS is required except for loopback development addresses. The `noura://auth/complete` return link only focuses Noura and refreshes its native sign-in flow. It carries no credentials and cannot authorize sync. If the browser blocks automatic opening, use **Return to Noura**. On macOS, test with the generated `.app` bundle: URL-scheme registration is part of the bundle, not plain `tauri dev`. Closing settings does not cancel login; cancelling explicitly, expiration, or quitting the application ends a pending attempt. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..8d9618c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,37 @@ +# Noura documentation + +Use these guides to run Noura, understand its file-backed workspace, and contribute to its plugins and services. Start with the guide for your task: + +- **Run Noura**: follow the [desktop setup instructions](../README.md#try-noura) +- **Contribute code**: read the [contributor conventions](../AGENTS.md) and [desktop verification guide](testing/local-alpha-acceptance.md) +- **Work with plugins**: learn the [activation lifecycle and capability contracts](architecture/plugin-runtime.md) +- **Understand workspace files**: read the [workspace format](workspace-format/v1.md) and [local-core architecture](architecture/local-core.md) +- **Understand AI access**: read about [provider consent, tool approval, and chat persistence](architecture/ai-runtime.md) +- **Run the sync service**: follow [server setup](../apps/server/README.md), then the [operations guide](../apps/server/OPERATIONS.md) + +## Workspace format references + +Treat these specifications as the public contract for durable workspace data. Rust owns parsing and deterministic serialization; TypeScript validators use the same [conformance fixtures](workspace-format/fixtures/): + +- [Workspace format v1](workspace-format/v1.md): manifests, managed Markdown, stable identifiers, and chats +- [Encrypted sync protocol](workspace-format/sync-v1.md): signed operations, key envelopes, file changes, and attachments +- [Collaboration protocol](workspace-format/collaboration.md): text generations, updates, and recovery +- [Encrypted checkpoints](workspace-format/checkpoints.md): snapshots and access transitions + +## Architecture references + +Use these documents to understand component responsibilities and the boundaries a change must preserve: + +- [Local core](architecture/local-core.md) +- [Plugin runtime](architecture/plugin-runtime.md) +- [AI runtime](architecture/ai-runtime.md) +- [Encrypted synchronization service](architecture/sync-server.md) + +## Security and provenance + +Follow the [security policy](../SECURITY.md) to report a vulnerability. Review the relevant boundaries and dependency requirements before changing them: + +- [Threat model](security/threat-model.md) +- [Dependency audit](security/dependency-audit.md) +- [Source provenance requirements](provenance/README.md) +- [Third-party notices](../THIRD_PARTY_NOTICES.md) diff --git a/docs/architecture/ai-runtime-spike.md b/docs/architecture/ai-runtime-spike.md deleted file mode 100644 index 6f22249..0000000 --- a/docs/architecture/ai-runtime-spike.md +++ /dev/null @@ -1,23 +0,0 @@ -# Pi runtime spike - -This temporary Phase 0 slice proves that Pi Agent Core can run inside Noura's WebView without access to a provider credential or direct network transport. - -## Flow - -1. The AI route creates a Pi `Agent` with a custom stream function. -2. The stream function creates a UUID operation ID and invokes a native Tauri Channel transport. -3. The native host sends ordered synthetic `delta`, `done`, or `aborted` frames. -4. The adapter rejects a mismatched operation ID or sequence and converts terminal native frames into Pi assistant messages. -5. Pi cancellation calls the native cancellation command. The native operation sends an `aborted` frame and removes its operation registry entry. - -The probe sends neither workspace content nor provider credentials. It uses no provider request, filesystem tool, shell tool, or persistent agent state. - -## Verification - -Run `bun run verify:pi-runtime` to build a standalone browser bundle and reject Node-only runtime imports. With Pi `0.84.4`, the standalone bundle is 285.97 kB uncompressed and 62.85 kB gzip. - -The application production build loads the probe only from the AI route. The current route chunk contribution is 106.48 kB gzip; reassess this cost before making the production agent surface generally available. - -## Mobile status - -The iOS and Android configuration overrides use `org.noura.app`. The generated iOS project is available locally, but a full iOS compile requires Xcode and the iPhoneOS SDK. Android generation and compilation require an installed Android SDK and NDK. Those toolchains are not currently available, so mobile WebView execution remains a release gate rather than a verified claim. diff --git a/docs/architecture/ai-runtime.md b/docs/architecture/ai-runtime.md index 54f14d2..7de6022 100644 --- a/docs/architecture/ai-runtime.md +++ b/docs/architecture/ai-runtime.md @@ -51,10 +51,13 @@ All mutation requests use expected revisions. External edits win and surface a c ## Web access -Web search and URL fetch are later, separate native capabilities. They accept only validated HTTPS destinations, reject private/local addresses and unsafe redirects, enforce size/type/timeout limits, return text-oriented extracted content with source attribution, and have their own one-time disclosure. They never use browser cookies, browser history, local URL schemes, or credential discovery. +Web search and URL fetch have a separate native authorization boundary. Access requires its own one-time disclosure and consent, in addition to AI provider consent. The native service applies these restrictions: -Noura does not enable Exa automatically. A September 2026 review found that its API requires an account and API key, grants use only to authorized users under its Terms, is subject to documented usage limits, and has usage-based pricing. Noura therefore cannot establish zero-configuration redistribution. Noura's native web service fails closed unless a compliant, explicitly configured provider and the separate web disclosure are present. +- Accept only validated HTTPS destinations +- Reject private or local addresses and unsafe redirects +- Enforce size, type, and timeout limits +- Return text-oriented content with source attribution -## Superseded sidecar direction +Web access never uses browser cookies, browser history, local URL schemes, or credential discovery. -The Noura AI runtime supersedes the former sidecar approach. Noura bundles Pi as a browser-safe first-party dependency in the WebView; the WebView reaches native provider access only through typed IPC. The Phase 0 compatibility record remains in [Pi runtime spike](./ai-runtime-spike.md). +Noura’s native web service fails closed until you configure a compliant provider and grant separate web-access consent. Provider configuration does not authorize a request on its own. diff --git a/docs/architecture/plugin-runtime.md b/docs/architecture/plugin-runtime.md index 1244547..9e7a8cb 100644 --- a/docs/architecture/plugin-runtime.md +++ b/docs/architecture/plugin-runtime.md @@ -1,6 +1,6 @@ # Plugin runtime architecture -The plugin platform layers the workspace on one kernel: `packages/plugin-sdk` defines capability contracts, `packages/workspace` adapts them to the typed client, and `apps/app` constructs the runtime inside the webview. First-party domains (`plugins/notes`, `tasks`, `calendar`, `projects`, `folders`) dogfood the same public surface a future ecosystem plugin receives. +Noura’s first-party notes, tasks, calendar, projects, folders, and AI plugins use shared capability contracts. `packages/plugin-sdk` defines those contracts, `packages/workspace` adapts them to the typed client, and `apps/app` constructs the runtime in the WebView: ```text apps/app state @@ -14,14 +14,28 @@ PluginContext ── capability-gated facade over NouraClient ## Activation lifecycle -- Similar to any other file, `.noura/workspace.yaml` is authoritative. The runtime never caches plugin state; `syncWithManifest` re-reads the manifest and reconciles the active set. -- A plugin activates only when its manifest id appears in `enabled_plugins`. The runtime ignores unknown ids in the manifest, so a workspace carrying ecosystem plugins opens on older builds. -- Plugin definitions may implement `deactivate(context)`. The host passes the same context instance the plugin saw during activation, so handlers and disposers captured then stay valid. Deactivation runs commands unregistering, AI tool/context removal, and event unsubscription through the disposers the capabilities already return. -- The desktop app re-syncs on startup, after `workspace:ready`, after `workspace:manifest-updated`, and on `file:changed`. The engine observes external edits to `.noura/workspace.yaml` through its watcher: the atomic-write journal suppresses the engine's own writes, and a genuine external change adopts the file into the engine snapshot and emits `workspace:manifest-updated` with source `external`, so plugin state follows the file even while the app is open. The Settings panel writes `enabled_plugins` through the engine's `manifest_update` (atomic, revision-checked against `updated`), so live deactivation and activation run through the same durable file mutation. Closing a workspace deactivates every plugin instead of leaving commands registered against a workspace that is gone. Navigation and route guards derive from the reconciled state: modules that are off simplify the workspace. +`syncWithManifest` treats `.noura/workspace.yaml` as authoritative and reconciles the active host set in this order: + +1. Read `enabled_plugins` from the durable manifest. Preserve unknown IDs in the manifest without activating them. +2. Deactivate active plugins whose IDs are absent from `enabled_plugins`. +3. Activate enabled first-party plugins that are not active. + +The desktop runtime reconciles at startup and after `workspace:ready`, `workspace:manifest-updated`, or `file:changed` events. The engine adopts external manifest edits and emits `workspace:manifest-updated` with source `external`. Its atomic-write journal suppresses notifications for its own writes. + +Settings updates `enabled_plugins` through the engine’s atomic, revision-checked `manifest_update` operation. Navigation and route guards follow the reconciled plugin state. + +Plugin definitions may implement `deactivate(context)`. The host supplies the activation context, and capability disposers unregister commands, remove AI tools and context, and unsubscribe events. Closing a workspace deactivates its plugins and removes their registrations. ## Capabilities -`PluginContext` exposes only what the plugin manifest declares: `workspace.files`, `workspace.objects`, `workspace.search`, `workspace.commands`, `workspace.events`, `workspace.storage`, `ai.tools`, and `ai.context`. Every call guards against undeclared capabilities before it reaches a service. Plugins never see transport internals, SQLite, or Rust types. +`PluginContext` exposes the capabilities declared by the plugin manifest: + +- `workspace.files` and `workspace.objects` +- `workspace.search` and `workspace.commands` +- `workspace.events` and `workspace.storage` +- `ai.tools` and `ai.context` + +Every call checks the declared capability before it reaches a service. Plugins never see transport internals, SQLite, or Rust types. ## Storage contract @@ -33,6 +47,13 @@ PluginContext ── capability-gated facade over NouraClient ## Boundaries +Keep dependency and transport responsibilities separate: + - `packages/workspace` may import first-party plugins; plugins never import `packages/workspace`. - The MCP server does not construct a plugin runtime. Plugin-contributed MCP tools are future work behind the same capability contracts. -- Rich UI contributions from plugins (a tree, boards, custom panes) do not exist yet. The **per-module sidebar registry** (`apps/app/src/lib/sidebar-modules.ts`) organizes shell-level surfaces: each module decides what lives in the sidebar for its routes, or opts out entirely, in which case those routes render full-width. The runtime gates contributions on whether the module's plugin is enabled, and sections read module-owned stores (the file browser reads the folders-gated tree, the tasks views read the shared tasks projection). A `workspace.views.register`-style capability is the intended upgrade path so plugins can own their own UI; the registry shape lets those entries replace the hardcoded ones without changing the renderer. + +## UI contributions + +Plugins do not yet contribute rich UI such as trees, boards, or custom panes. The shell uses the per-module sidebar registry in `apps/app/src/lib/sidebar-modules.ts`; each module defines sidebar content for its routes or opts out so its routes render full width. + +The registry includes a module only when its plugin is enabled. Sidebar sections read module-owned stores: the file browser reads the folders-gated tree, while task views read the shared tasks projection. A future `workspace.views.register` capability can replace these hardcoded entries without changing the renderer. diff --git a/docs/architecture/sync-server.md b/docs/architecture/sync-server.md index 3969858..484c777 100644 --- a/docs/architecture/sync-server.md +++ b/docs/architecture/sync-server.md @@ -1,29 +1,25 @@ # Encrypted synchronization service -## Dependency decision (2026-09-05) - -We inspected Syncular at commit `e4ae85f33617909ad41a2af1a9c7f5296fa77b00` in https://github.com/syncular/syncular (Apache-2.0). We did not copy any donor source. - -The native client owns a rusqlite connection. Its private `apply_section_body` and `apply_commit_changes` methods apply remote changes inside the client's SQLite observation transaction. There is no public file-commit acknowledgment hook. Its server `pruneCommitLog` considers cursor age and a retained-count floor; it has no client-created encrypted snapshot coverage predicate. - -These are static compatibility findings, not results of a working Syncular prototype. Adopting the complete native client would require modifying its persistence boundary. Using only the server would still require a separate Noura client and snapshot retention protocol. We therefore use the approved fallback: Bun/Hono, PostgreSQL, and a Noura-owned opaque-operation protocol. This supersedes the provisional Syncular recommendation, not the file-first or encryption invariants. SuperSync and Secsync remain research references; we have not copied or adapted their source. +Noura’s sync service uses Bun, Hono, and PostgreSQL to route signed, encrypted operations between authorized clients. Clients own workspace content keys and canonical file writes. Read the [server setup guide](../../apps/server/README.md) to run the service. ## Trust boundary -The service receives signed ciphertext envelopes, never workspace plaintext. Authentication and authorization are separate from possession of content keys. An operation's signature binds all routing fields and ciphertext. Database transactions serialize workspace changes, including revocation, before the service returns an acknowledgment. Operation IDs are durable retry identities. +Clients send signed ciphertext envelopes rather than plaintext workspace content. Authentication and authorization are separate from possession of content keys. An operation’s signature binds its routing fields and ciphertext. -Client keys do not belong on this server. Native credentials belong in the OS credential store. The server can see opaque workspace, object and device IDs, authorization relationships, ciphertext length, and traffic timing. +Database transactions serialize workspace changes, including revocation, before the server returns an acknowledgment. Operation IDs are durable retry identities. Native credentials remain in the operating system’s credential store. -## Release status +The server can observe this metadata: -This service is under implementation. Do not describe it as production ready or as integrated desktop synchronization until native durable application, identity/recovery, collaborative editing, granular sharing, public viewing, and the complete acceptance gates are verified. Read the service README for the currently executable surface and verification commands. +- Opaque workspace, object, and device IDs +- Authorization relationships +- Ciphertext length and traffic timing -The current implementation includes browser-approved native account sign-in, signed access policies, encrypted public snapshots and viewing, native file transport, age key envelopes, and a durable file journal. See [`sync-v1.md`](../workspace-format/sync-v1.md) for the file and key protocol. Desktop Settings now exposes background workspace sync, pause/resume, joining an existing member workspace in an empty local folder, explicit device-fingerprint approval, and same-path conflict review. The native coordinator retains consent, sender pins, encrypted keys, and its outbox across restarts. It never acquires trust from an unverified server key. Fingerprints bind the enrolled account ID, device ID, Ed25519 key, and age recipient together. Device account labels cannot change an existing approval's scope. +The server does not hold client content keys. Clients must authenticate and decrypt incoming content; the server cannot prove that a malicious client encrypted submitted bytes. -A writer can back up its own signed recipient envelope before uploading its first operation. This does not grant another account access or rotate an epoch; authorization and epoch changes still require a complete signed access policy. Shared coordinator passes use that policy to distribute keys only to explicitly approved recipients. The key-delivery endpoint now permits an existing object writer to supply immutable envelopes to already-authorized devices. This closes the editor-created-object gap without granting editors permission-management rights. Viewers use a receive-only transport after verifying their signed role. Revoked recipients stop further shared uploads until keys are rotated. +## Service scope -File payload version 2 adds explicit conflict resolution over reviewed content revisions. Version 1 remains unchanged. This is necessary because a single-base file update cannot converge the two reviewed branches after an offline conflict. It does not permit overwriting an unrelated later revision and is not a substitute for the planned collaborative text protocol. +The service supports the encrypted transport described in [`sync-v1.md`](../workspace-format/sync-v1.md): signed opaque operations, access policies, encrypted key envelopes, attachment ciphertext, and public encrypted snapshots. The native client owns canonical file application, local consent, trust pins, credentials, and durable journal state. -Payload version 3 adds encrypted attachment descriptors and age streaming files. The server reuses the MIT-licensed Tus server and file store for resumable transfer; PostgreSQL authorization and durable-file acknowledgments wrap that dependency. See the workspace-format specification for exact descriptor and encryption rules. +The service verifies authentication and authorization, serializes workspace changes, and stores opaque protocol records. It does not own workspace content keys or canonical workspace files. Experimental collaboration capability routes remain disabled by standard service startup; durable HTTP pull and acknowledgment remain authoritative when those routes are enabled for testing. -Workspace owners can create seven-day invitation links in desktop Settings. Recipients accept through an authenticated browser session, then connect a desktop device. Acceptance alone creates no membership or key access. The owner compares the recipient's device fingerprints, approves them locally, and grants access by signing the next complete access policy with recipient key envelopes. The owner can revoke pending or accepted invitations. Each workspace permits at most 100 active invitations; the review list prioritizes active invitations over history. +The service remains experimental. Follow the [operations guide](../../apps/server/OPERATIONS.md) for deployment, backup, and validation requirements. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 2777df3..a65e08f 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -4,6 +4,14 @@ Noura protects workspace bytes, provider credentials, stable identity, and the integrity of writes that follow an external edit. The Initial MVP assumes a trusted local user and trusted first-party bundled plugins. It does not claim isolation from malware running under the same operating system account. +## Local and network-enabled scope + +The local core runs workspace operations without a Noura account or network service. It reads and writes canonical workspace files locally, and the desktop host and standalone Model Context Protocol (MCP) server call the same services. + +Network-enabled features add separate boundaries. You authorize the client before it sends workspace content to an AI provider. The native web-access boundary requires explicit consent and validates public HTTPS destinations. + +Experimental account and sync services process authentication metadata and encrypted workspace content. Clients retain workspace content keys; managed sync must never require plaintext workspace bytes on the server. Local workspace operations remain independent of these services. + ## Trust boundaries - Workspace paths and file contents are untrusted input. @@ -22,25 +30,10 @@ Provider settings contain opaque credential references. The OS credential store ## Known limitations -- **Relay completeness.** Managed sync is end-to-end encrypted: a relay only ever - sees ciphertext, and every operation is signature-verified against a trusted - device key. A malicious or compromised relay can still omit operations or - advance a client's pull cursor, because the client accepts the server's - monotonic cursor without a cryptographic completeness commitment. This is an - availability and integrity limit, not a confidentiality one; closing it - requires a protocol-level commitment and a larger review. -- **Local symlink races.** Mutation paths reject symlink components and validate - the destination before writing, but a concurrent process could replace a parent - directory between validation and the write. The threat model excludes malware - running under the same operating-system account. PDF reads close this window by - opening each path component relative to an already-open directory with - `NOFOLLOW`. -- **Plugin isolation.** Community plugins do not run. The host validates and - freezes every manifest, snapshots it at activation, and enforces declared - capabilities at each context call. Admitted plugin code would still execute in - the application WebView, so a real sandbox remains a prerequisite for - third-party loading. - -## Deferred risks - -The Initial MVP does not load community plugins, serve remote MCP transports, synchronize data, or expose accounts. Those features require separate authorization, sandboxing, network, and encryption reviews before implementation. +- **Relay completeness.** Managed sync is end-to-end encrypted: a relay only ever sees ciphertext, and every operation is signature-verified against a trusted device key. A malicious or compromised relay can still omit operations or advance a client's pull cursor, because the client accepts the server's monotonic cursor without a cryptographic completeness commitment. This is an availability and integrity limit, not a confidentiality one; closing it requires a protocol-level commitment and a larger review. +- **Local symlink races.** Mutation paths reject symlink components and validate the destination before writing, but a concurrent process could replace a parent directory between validation and the write. The threat model excludes malware running under the same operating-system account. PDF reads close this window by opening each path component relative to an already-open directory with `NOFOLLOW`. +- **Plugin isolation.** Community plugins do not run. The host validates and freezes every manifest, snapshots it at activation, and enforces declared capabilities at each context call. Admitted plugin code would still execute in the application WebView, so a real sandbox remains a prerequisite for third-party loading. + +## Deferred and release-gated risks + +The Initial MVP does not load community plugins or serve remote MCP transports. Those features require separate authorization and sandboxing reviews before implementation. Sync, account, and collaboration paths remain experimental and require their documented network, encryption, cross-platform, and independent-review release gates before user-facing release. diff --git a/docs/testing/local-alpha-acceptance.md b/docs/testing/local-alpha-acceptance.md index b876964..1c3b2d4 100644 --- a/docs/testing/local-alpha-acceptance.md +++ b/docs/testing/local-alpha-acceptance.md @@ -73,12 +73,14 @@ Use this runbook for contributor and release review of the desktop Local Alpha. 1. In Settings, choose **Rebuild index**. Confirm the button stays disabled while running and a success toast appears. Confirm every projection refreshes. 2. Record each object's ID, path, body, and dated properties. 3. Quit Noura. -4. Find the workspace's local index by matching the ID from `.noura/workspace.yaml`: +4. Find the workspace’s local index by matching the ID from `.noura/workspace.yaml`. On macOS, search the application-data directory with: ```sh find "$HOME/Library/Application Support" -path "*/workspaces/*/index.sqlite" -print ``` + On other platforms, locate the index in Noura’s operating system application-data directory. Match the workspace ID before deleting any index. + 5. Delete only that workspace's `index.sqlite`, then reopen Noura and the disposable workspace. 6. Confirm notes, tasks, projects, IDs, paths, bodies, search results, Calendar results, and project boards match the recorded state. Calendar may return to Month because its view preference is disposable. diff --git a/docs/workspace-format/checkpoints.md b/docs/workspace-format/checkpoints.md index aa7bac4..ee2a7f5 100644 --- a/docs/workspace-format/checkpoints.md +++ b/docs/workspace-format/checkpoints.md @@ -52,4 +52,4 @@ At a text-generation boundary, native state compares the acknowledged baseline w Native committed checkpoints are retrieved against the accepted policy hash chain and persist a rollback floor. Final fingerprint approval prepares and commits the same rotation transaction automatically, and normal sync resumes it after interruption. Attachment migration and writer-created object activation use signed, resumable checkpoint transactions; capability-enrolled device-recipient changes use the full-object rotation coordinator. User-facing member removal and object-grant commands still need to be connected to that coordinator. Inline ciphertext retains the existing 1 MiB operation bound. Version-2 transitions and activations support transaction-bound blob staging and commit-time completeness checks. -Folder inheritance, scoped invitations, partial joins, sharing UI, and expanded public publishing are not implemented by this protocol. The experimental native realtime client supplies authenticated notifications and encrypted presence, while durable HTTP pulls and acknowledgements remain authoritative. The experimental text coordinator and editor adapter are described in [collaboration.md](collaboration.md). The PostgreSQL 16 restore rehearsal has passed with staged and committed transitions, checkpoint/blob manifests, resumable uploads, and restored protocol state. Cross-platform invitation-to-collaboration acceptance, the 20-editor WAN latency target, the Windows worker capability confinement, native multi-process CRDT load, and an independent security implementation review remain release gates. The implementation and tests were author-performed, not an independent audit. +Folder inheritance, scoped invitations, partial joins, sharing UI, and expanded public publishing are not implemented by this protocol. The experimental native realtime client supplies authenticated notifications and encrypted presence, while durable HTTP pulls and acknowledgements remain authoritative. The experimental text coordinator and editor adapter are described in [collaboration.md](collaboration.md). Production startup keeps these routes disabled. The implementation and tests were author-performed, not an independent security audit. diff --git a/docs/workspace-format/collaboration.md b/docs/workspace-format/collaboration.md index 2f8db66..fd5cd54 100644 --- a/docs/workspace-format/collaboration.md +++ b/docs/workspace-format/collaboration.md @@ -18,7 +18,7 @@ Object activation version 1 atomically publishes a writer-created object at epoc Text plaintext contains `{ version: 1, objectId, generation, updates }`. Updates are base64 Yjs update-v1 bytes. The root shared text is named `content`. Yrs uses UTF-16 offsets, matching Yjs and CodeMirror. Tests consume independently generated `fixtures/yjs-text-v1.json` and `fixtures/yrs-text-v1.json` for Unicode, deletion, state vectors and local undo. Cargo and Bun pin versions. No upstream implementation source was copied. -The native coordinator validates a candidate document before writing. It limits text to 8 MiB and encoded document history to 32 MiB. Restore and candidate application cross a dedicated native worker process that receives only CRDT bytes and text, never credentials, network handles, workspace paths, or file writers. Its environment is cleared and its working directory is the filesystem root. Release workers enter Apple's pure-computation Seatbelt profile on macOS; Linux workers install a no-new-privileges seccomp filter that rejects networking, filesystem opens and mutations, process creation, and execution. Windows hosts assign a 512 MiB, kill-on-close Job Object, but workers fail closed before decoding because equivalent AppContainer network/filesystem confinement is not implemented and validated. Requests have a five-second deadline, a 512 MiB process limit (enforced by `RLIMIT_AS` on Linux, physical-footprint monitoring on macOS, and the Job Object on Windows), an aggregate preflight budget including an 8x decoded-structure allowance, and a bounded 16-worker concurrency ceiling. Worker panic, timeout, saturation, invalid shared types, missing references, attributes, invalid Unicode text, or an oversized result fails before canonical bytes or recovery intent change. A debug-only explicit test marker permits the worker to inherit an already active host sandbox when a nested macOS profile is refused; release builds fail closed. Windows capability confinement and cross-platform adversarial execution remain release blockers. Text-operation envelopes above 700 KiB use a versioned encrypted blob descriptor; the coordinator flushes and renames the ciphertext by digest before enqueuing the signed operation, and receivers verify and decrypt the blob before candidate application. +The native coordinator validates a candidate document before writing. Text is limited to 8 MiB and encoded document history to 32 MiB. Restore and candidate application cross a dedicated native worker process that receives only CRDT bytes and text, never credentials, network handles, workspace paths, or file writers. Its environment is cleared and its working directory is the filesystem root. Release workers enter Apple's pure-computation Seatbelt profile on macOS; Linux workers install a no-new-privileges seccomp filter that rejects networking, filesystem opens and mutations, process creation, and execution. Windows hosts assign a 512 MiB, kill-on-close Job Object, but workers fail closed before decoding because equivalent AppContainer network/filesystem confinement is not implemented and validated. Requests have a five-second deadline, a 512 MiB process limit (enforced by `RLIMIT_AS` on Linux, physical-footprint monitoring on macOS, and the Job Object on Windows), an aggregate preflight budget including an 8x decoded-structure allowance, and a bounded 16-worker concurrency ceiling. Worker panic, timeout, saturation, invalid shared types, missing references, attributes, invalid Unicode text, or an oversized result fails before canonical bytes or recovery intent change. A debug-only explicit test marker permits the worker to inherit an already active host sandbox when a nested macOS profile is refused; release builds fail closed. Windows capability confinement and cross-platform adversarial execution remain release blockers. Text-operation envelopes above 700 KiB use a versioned encrypted blob descriptor; the ciphertext is flushed and renamed by digest before the signed operation is enqueued, and receivers verify and decrypt the blob before candidate application. For managed Markdown, the shared text is the body; native canonical Markdown serialization remains authoritative. Any valid editor body detail normalized by canonical serialization remains in the durable CRDT draft. General text retains BOM and newline conventions. Legacy raw/body mutations cannot bypass an active text generation. Desktop and MCP creates and updates, desktop moves/deletes, editor transactions, and external saves use the native per-object coordinator whenever collaboration is active. Before canonical normalization, the application flushes the exact external bytes to a recovery snapshot; invalid managed documents, protected-field changes, deletions, and format-only ambiguity remain untouched and enter durable review. The application captures a writer-created canonical object into the signed activation state immediately after its durable file save; if capture is temporarily unavailable, the successful save returns a recovery warning and the normal sync scan resumes activation. diff --git a/docs/workspace-format/fixtures/activation-v1.json b/docs/workspace-format/fixtures/activation-v1.json index 3873f1d..0b1b011 100644 --- a/docs/workspace-format/fixtures/activation-v1.json +++ b/docs/workspace-format/fixtures/activation-v1.json @@ -1,59 +1,59 @@ { - "domain": "noura.sync.object-activation", - "version": 1, - "vectors": [ - { - "name": "mixed-age-web-envelopes", - "activation": { - "version": 1, - "activationId": "activation_fixture", - "workspaceId": "workspace", - "policyRevision": "7", - "coveredSequence": "12", - "capabilityDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "deviceId": "device_signer", - "document": { - "generation": "generation", - "mode": "text" - }, - "envelopes": [ - { - "deviceId": "device_age", - "wrappedKey": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "signature": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg==" - }, - { - "deviceId": "device_browser", - "wrappedKey": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", - "signature": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ==", - "construction": "web", - "recipientPublicKey": "BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgY=", - "ephemeralPublicKey": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", - "salt": "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg=", - "nonce": "CQkJCQkJCQkJCQkJ" - } - ], - "checkpoint": { - "version": 1, - "generation": "generation", - "coveredSequence": "12", - "payload": { - "version": 1, - "operationId": "operation_fixture", - "workspaceId": "workspace", - "objectId": "object", - "deviceId": "device_signer", - "epoch": 1, - "policyRevision": "7", - "nonce": "CgoKCgoKCgoKCgoK", - "ciphertext": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws=", - "signature": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA==" - }, - "signature": "DQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ==" - }, - "signature": "Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg==" - }, - "expected_signing_bytes": "[\"noura.sync.object-activation\",1,\"activation_fixture\",\"workspace\",\"7\",\"12\",\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"device_signer\",[\"generation\",\"text\"],[[\"device_age\",\"AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=\",\"AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg==\"],[\"device_browser\",\"BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ==\",\"web\",\"BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgY=\",\"BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\",\"CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg=\",\"CQkJCQkJCQkJCQkJ\"]],\"d2252a6611dba989063a0fa0a9e1d9c254813e0dc844a471bcac95a90b45c016\"]" - } - ] + "domain": "noura.sync.object-activation", + "version": 1, + "vectors": [ + { + "name": "mixed-age-web-envelopes", + "activation": { + "version": 1, + "activationId": "activation_fixture", + "workspaceId": "workspace", + "policyRevision": "7", + "coveredSequence": "12", + "capabilityDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "deviceId": "device_signer", + "document": { + "generation": "generation", + "mode": "text" + }, + "envelopes": [ + { + "deviceId": "device_age", + "wrappedKey": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "signature": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg==" + }, + { + "deviceId": "device_browser", + "wrappedKey": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", + "signature": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ==", + "construction": "web", + "recipientPublicKey": "BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgY=", + "ephemeralPublicKey": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", + "salt": "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg=", + "nonce": "CQkJCQkJCQkJCQkJ" + } + ], + "checkpoint": { + "version": 1, + "generation": "generation", + "coveredSequence": "12", + "payload": { + "version": 1, + "operationId": "operation_fixture", + "workspaceId": "workspace", + "objectId": "object", + "deviceId": "device_signer", + "epoch": 1, + "policyRevision": "7", + "nonce": "CgoKCgoKCgoKCgoK", + "ciphertext": "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws=", + "signature": "DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA==" + }, + "signature": "DQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ==" + }, + "signature": "Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg==" + }, + "expected_signing_bytes": "[\"noura.sync.object-activation\",1,\"activation_fixture\",\"workspace\",\"7\",\"12\",\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"device_signer\",[\"generation\",\"text\"],[[\"device_age\",\"AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=\",\"AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg==\"],[\"device_browser\",\"BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ==\",\"web\",\"BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgY=\",\"BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\",\"CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg=\",\"CQkJCQkJCQkJCQkJ\"]],\"d2252a6611dba989063a0fa0a9e1d9c254813e0dc844a471bcac95a90b45c016\"]" + } + ] } diff --git a/docs/workspace-format/fixtures/browser-device-v1.json b/docs/workspace-format/fixtures/browser-device-v1.json index 75fc8cb..4ebf850 100644 --- a/docs/workspace-format/fixtures/browser-device-v1.json +++ b/docs/workspace-format/fixtures/browser-device-v1.json @@ -1,76 +1,76 @@ { - "enrollment": { - "domain": "noura.device.enroll.web", - "vectors": [ - { - "account_id": "account_owner", - "challenge": "challenge_browser_device_1", - "device_id": "device_browser", - "expected_base64": "Bf2d0nAGECoOK0i9+n4SJ6cII8uC6pylJyNody/bsW0Wpwx6GU+xY/GGPg9sfx4TJzOfimeSAdpZyiiY7fn2Cw==", - "expected_hex": "05fd9dd27006102a0e2b48bdfa7e1227a70823cb82ea9ca5272368772fdbb16d16a70c7a194fb163f1863e0f6c7f1e1327339f8a679201da59ca2898edf9f60b", - "name": "browser-device-enroll", - "origin": "https://app.noura.example", - "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=" - } - ], - "version": 1 - }, - "fingerprint": { - "domain": "noura.device.card.web", - "vectors": [ - { - "account_id": "account_owner", - "device_id": "device_browser", - "expected_hex": "981952d2679b8017a5e61c404bb37cab1b27a62ad89e0bb4715c431c3e0a91e3", - "name": "browser-device-card", - "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - } - ], - "version": 1 - }, - "recipient_prefix": "x25519:", - "recipients": { - "invalid": [ - { - "expected_error": "sync_invalid_recipient", - "name": "missing_prefix", - "recipient": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=" - }, - { - "expected_error": "sync_invalid_recipient", - "name": "wrong_base64", - "recipient": "x25519:not-base64!!" - }, - { - "expected_error": "sync_invalid_recipient", - "name": "short_31_bytes", - "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw==" - }, - { - "expected_error": "sync_invalid_recipient", - "name": "long_33_bytes", - "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH" - }, - { - "expected_error": "sync_invalid_recipient", - "name": "age_form", - "recipient": "age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" - } - ], - "valid": [ - { - "name": "raw-recipient-zero", - "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "recipient": "x25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - }, - { - "name": "raw-recipient-seeded", - "public_key": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", - "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=" - } - ] - } + "enrollment": { + "domain": "noura.device.enroll.web", + "vectors": [ + { + "account_id": "account_owner", + "challenge": "challenge_browser_device_1", + "device_id": "device_browser", + "expected_base64": "Bf2d0nAGECoOK0i9+n4SJ6cII8uC6pylJyNody/bsW0Wpwx6GU+xY/GGPg9sfx4TJzOfimeSAdpZyiiY7fn2Cw==", + "expected_hex": "05fd9dd27006102a0e2b48bdfa7e1227a70823cb82ea9ca5272368772fdbb16d16a70c7a194fb163f1863e0f6c7f1e1327339f8a679201da59ca2898edf9f60b", + "name": "browser-device-enroll", + "origin": "https://app.noura.example", + "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", + "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=" + } + ], + "version": 1 + }, + "fingerprint": { + "domain": "noura.device.card.web", + "vectors": [ + { + "account_id": "account_owner", + "device_id": "device_browser", + "expected_hex": "981952d2679b8017a5e61c404bb37cab1b27a62ad89e0bb4715c431c3e0a91e3", + "name": "browser-device-card", + "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + } + ], + "version": 1 + }, + "recipient_prefix": "x25519:", + "recipients": { + "invalid": [ + { + "expected_error": "sync_invalid_recipient", + "name": "missing_prefix", + "recipient": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=" + }, + { + "expected_error": "sync_invalid_recipient", + "name": "wrong_base64", + "recipient": "x25519:not-base64!!" + }, + { + "expected_error": "sync_invalid_recipient", + "name": "short_31_bytes", + "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw==" + }, + { + "expected_error": "sync_invalid_recipient", + "name": "long_33_bytes", + "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH" + }, + { + "expected_error": "sync_invalid_recipient", + "name": "age_form", + "recipient": "age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + } + ], + "valid": [ + { + "name": "raw-recipient-zero", + "public_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "recipient": "x25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "name": "raw-recipient-seeded", + "public_key": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=", + "recipient": "x25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=" + } + ] + } } diff --git a/docs/workspace-format/fixtures/browser-key-v1.json b/docs/workspace-format/fixtures/browser-key-v1.json index da429b3..5d99156 100644 --- a/docs/workspace-format/fixtures/browser-key-v1.json +++ b/docs/workspace-format/fixtures/browser-key-v1.json @@ -1,194 +1,194 @@ { - "domain": "noura.sync.key.web", - "info": "noura.sync.key.web.v1", - "invalid": [ - { - "envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "YVHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "expected_error": "sync_invalid_signature", - "name": "tampered_signature", - "operation": "verify", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "WdS4Xa8zS/Oe2++jhR9dB3baff+4CSuybSJXd+h3xlz911wmhtkYzYgfBPTk/bJ4PrjNVjAHeY87hflFEbgHAQ==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcc=" - }, - "expected_error": "sync_key_unwrap_failed", - "name": "tampered_wrapped_key", - "operation": "unwrap", - "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "envelope": { - "device_id": "device_browser_other", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "HAiuK0ACNhS7SRwapeL5R6za5WB8YTPKQX/GkmvwtGysi6XXXV9vd2c2YhbRe5d+26X9cWEjJLD+B8UMl9ZVCg==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "expected_error": "sync_key_unwrap_failed", - "name": "tampered_aad_field", - "operation": "unwrap", - "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "expected_error": "sync_key_unwrap_failed", - "name": "wrong_recipient_secret", - "operation": "unwrap", - "recipient_secret": "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_invalid_key_envelope", - "inputs": { - "device_id": "device_browser", - "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "recipient_public": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signing_device": "device_signer", - "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", - "workspace_id": "workspace" - }, - "name": "low_order_recipient_public", - "operation": "wrap" - }, - { - "envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "not-base64!!", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "expected_error": "sync_invalid_base64", - "name": "bad_base64", - "operation": "verify", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg==", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "expected_error": "sync_invalid_base64", - "name": "wrong_length", - "operation": "verify", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_invalid_epoch", - "inputs": { - "device_id": "device_browser", - "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", - "epoch": 0, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "recipient_public": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signing_device": "device_signer", - "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", - "workspace_id": "workspace" - }, - "name": "epoch_zero", - "operation": "wrap" - } - ], - "object_key_domain": "noura.sync.object-key", - "valid": [ - { - "expected_envelope": { - "device_id": "device_browser", - "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "signing_device": "device_signer", - "workspace_id": "workspace", - "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" - }, - "inputs": { - "device_id": "device_browser", - "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "recipient_public": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "signing_device": "device_signer", - "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", - "workspace_id": "workspace" - }, - "name": "browser-key-v1-basic", - "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", - "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - } - ], - "version": 1 + "domain": "noura.sync.key.web", + "info": "noura.sync.key.web.v1", + "invalid": [ + { + "envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "YVHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "expected_error": "sync_invalid_signature", + "name": "tampered_signature", + "operation": "verify", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "WdS4Xa8zS/Oe2++jhR9dB3baff+4CSuybSJXd+h3xlz911wmhtkYzYgfBPTk/bJ4PrjNVjAHeY87hflFEbgHAQ==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcc=" + }, + "expected_error": "sync_key_unwrap_failed", + "name": "tampered_wrapped_key", + "operation": "unwrap", + "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "envelope": { + "device_id": "device_browser_other", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "HAiuK0ACNhS7SRwapeL5R6za5WB8YTPKQX/GkmvwtGysi6XXXV9vd2c2YhbRe5d+26X9cWEjJLD+B8UMl9ZVCg==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "expected_error": "sync_key_unwrap_failed", + "name": "tampered_aad_field", + "operation": "unwrap", + "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "expected_error": "sync_key_unwrap_failed", + "name": "wrong_recipient_secret", + "operation": "unwrap", + "recipient_secret": "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_invalid_key_envelope", + "inputs": { + "device_id": "device_browser", + "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "recipient_public": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signing_device": "device_signer", + "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + "workspace_id": "workspace" + }, + "name": "low_order_recipient_public", + "operation": "wrap" + }, + { + "envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "not-base64!!", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "expected_error": "sync_invalid_base64", + "name": "bad_base64", + "operation": "verify", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg==", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "expected_error": "sync_invalid_base64", + "name": "wrong_length", + "operation": "verify", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_invalid_epoch", + "inputs": { + "device_id": "device_browser", + "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", + "epoch": 0, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "recipient_public": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signing_device": "device_signer", + "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + "workspace_id": "workspace" + }, + "name": "epoch_zero", + "operation": "wrap" + } + ], + "object_key_domain": "noura.sync.object-key", + "valid": [ + { + "expected_envelope": { + "device_id": "device_browser", + "ephemeral_public_key": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "recipient_public_key": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "signing_device": "device_signer", + "workspace_id": "workspace", + "wrapped_key": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=" + }, + "inputs": { + "device_id": "device_browser", + "ephemeral_secret": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "recipient_public": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "signing_device": "device_signer", + "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + "workspace_id": "workspace" + }, + "name": "browser-key-v1-basic", + "recipient_secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", + "trusted_signer_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + } + ], + "version": 1 } diff --git a/docs/workspace-format/fixtures/browser-recovery-v1.json b/docs/workspace-format/fixtures/browser-recovery-v1.json index 5b1d54c..2c63af1 100644 --- a/docs/workspace-format/fixtures/browser-recovery-v1.json +++ b/docs/workspace-format/fixtures/browser-recovery-v1.json @@ -1,37 +1,37 @@ { - "format": "noura.browser-recovery-kit", - "version": 1, - "test_only": true, - "note": "Test-only browser recovery-kit fixture. Passphrases and keys are public test vectors; never use them.", - "recovery_passphrase": "browser-recovery-test-passphrase", - "device_passphrase": "browser-device-test-passphrase", - "kit": { - "format": "noura.browser-recovery-kit", - "version": 1, - "createdAt": "2026-01-01T00:00:00.000Z", - "kdf": "pbkdf2-sha256", - "iterations": 310000, - "salt": "LC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSks=", - "nonce": "TE1OT1BRUlNUVVZX", - "ciphertext": "otBwbZTU3dduyDkPxA7ctNRl2Pzi3FEoekjdEAhf21VsdOse7TjpSpOTtJqQWtspBi3mDjyEt0ebFAWWgto4Kao5FOADTt4a3enZ0E93SDz9c3WyzSL4JkE+tDnIlysGSV8x3XYfDII4T/dEcoEkeVrPM7oHma0E8A0VI9XmTiicwXQlMQTzCpoFbQrehEti1zoArMxb8BhPF8DlxzrpFcD31bNcwKuekXe2DpNCMrNF9TtOLxkJ1TLvUHu7PQr7diK+nUxgDWQmlkmRjiC3iWlZQFcKAvfEFZg9dqKjZ7Ubyji9bS5/lYzvHUW9bZRlkEWArfITYhANur4LzKGqKkqhyXLut8fBkw+EDdABnBvhK7a7O8GP1nEOEgmfZW8zpzkqc54Zl3DC6E7TUlVkALMb4r+5ZE6szjZkVLimc+9Eg3oTwdGt+1cAjlXyFSQ7s9GFOBHF2bRVTawh2M2+42zNzR36HhWdUCy09PePnHGwzMcttEiiShYorhnjoNmYf1yPqCZtYxeuUC44y5NW5SfTQstrc+OjOXmj94j9+pInPL5uklLA911Kh04F9iqCSYTyR/kBR+ejxMlyHySup2Xypvfl2DQ+ni7MvPQtuHc8geL/JtvIeNnejTn1C4xVj1ok6xoyEkgsC/9i+COKwPxLFZrmRl6vcR4JYkrTZkGsp1nfi2b/BAACcwWQr0qPljHc6p29/TB/3wnAdASyG+pZady6XeEM3QbgL/XdiZGvcEcd9WcTxpAcaNsTYtKn5puWlvXSCicOejXphQgZkw+zIzmWX36jh1QOznOdGjKklCFJX3eapRJASbdlacFLGyBE3hA4xgSDuukt8CKsu0FFWO40qv9kl3dl2+ZD3UsIjcMHcrvvhT4zIUWIsJgRQql8y8QfVphY8jn8vMnQuSnr5FfDe0zt427j/JYdpEcfXzK8u0DfeTt0XlfxLLpp2+RsIvsqAKTNY/3iNf2QEJYt+0H0bz4rXK38+ytliEKjNNrpbmLA3SdgRGgo8IHnPgC8FH/8L7Ll64ZDrA+AZD3juL3+vfYsWuZoijb0BtdWoCIkeXXOGPOlEskboUbL48x+yVEsN2vzvDKs2aRo4OSOTbpzVBMC0XvZB8ZLWufa2UqhYgVVrglF9VJCn2K11ZjZXcHFOMD6avUdZexzfSu2ep40Jtp0jll/HPePIgg6erjvFMzXdE8wE9lcLctMfNUKS1N5vjZrSvqxcNlT0R8TSWnXMT+mh8/Ui5EBFDkyfKK/Z8wgyuPFnuCiu2Je/R0ThyuWk1HTPtqXlKvqFQPA/GFhkDnV0kSMDJlkknJ84orsuSkv2C7bypD040BJvHT6fQbDjSQw2u88aCvKCOvBZfJXtqq2LRMOwcY9NAHlQj0QQ3kjC/kpGCOfJH89VPbSZaGeaPFftvoKwX158e51Z4N0AHWiRTZ5xXG3Tzexdf4QrC1nQX5UGtRblZjJxCfGQX7mUqkuFX8LXun8AS2YjTOhpaAx2RNuZN0D0YlOLN50blt6dF+GWovxVb6WahA/dEV1UhaGrb3FazVkhqKgcJ5PQLuneV71e0DXIrQaQki5X+jIcRRuF/MNjJDjNRmdfjl/V5ILVhGBYxHXudlEavhz9uoDBFTPSu0N77GlGHhGsE+t/a7TdSLvq75tiQnJ7jcMwyGf1U8OKXTx8I+F3nRgECHRMEU/3jCvPdcnEQE3y2z7sS1Nb38TyJKv1sdZUk2iyQFpYxdc/H7BWBWvcvQU+mOLgD1Y5BKtuYtupUtlYHRNnVjn8JKkNRmLLoEF9uqjO/gouVQaEKjO6D2nHFx6cYvlZX4/XjBfACj8zA0CPh7iUvCTiKkGfiYQRhwj9DT5X7WX5vNvrOCyXxFZCItbb6s2yXgQxe4yqSiquDXltUS7kUlAarGzEt0P5LqzYKmL6n9zS/DOdWPehZnRLK/Vj8kY0g8r1DA4WMEehilFbl/AfccHzHpxJ8hWF/pU10rqLXbCCYIFF4xOnN6aIztgRbZtIVyQQ6lWBp0Zmz6MW+GxOyhHyFVR7YAVdpGABBOHGMQPlvQWOdHMZ9PfAsKLugmoE8su75Eqd+NU6Pw+YLdkqPMd87cRZYa21/yxxbLx0ooOHfd382ZQZ7gQH0uwXLtsMqRL72jSt6u4vF6l1UU6FrrfDZqGpNK+whIGlPoDabUlyq0wD/xhaaI6KVGzzJA7O5Kudf20rE4nt+GyWZK1RddIJJTfqRnnegvjxN+mzbaz9H6QoWprF0iyfjugJ+ZJIDB0tbVwhg+btW3O/N6C3YinfqtBshbnKjQtqlLCZBmwHyyACjrCXdLZa5Hn9LsqtPOEjrFE56rqE4k2rNM5Bwq7yGSqFYVrSKidxOcyfy51LN8r8MNv07m6r1rrVCDY6FzQeKq7AUfT6cggMz4yUXBRK7S2VnEvREl5cyai7Ui8mYtLSqjiHWiszMAmHXo7kUfBXTlZiJyI03uaftqmNOrpIyTqkF44z01IHRIaqaJLcQ9mbRv3e3/MaUDk9hU+e7adIV44eEqyWUmKpKzdo/i5QC02B2Hbj5tTlYAke7Nuf5476RTEhTb2dPL5xu1LUdE4wcGzetu3zIc3MUDztYyOFRXlBtKbge/LbLSXDkCYW6z3cHmiucYn8e6ELTACCS3mVOBiAUeGNLgHXB8Cm7N9husalD+bZ0meIUMHGQxDwusB6wbBkTC2pGJ7iNSW0jgTrhek7g57B1Vj3soJNvYtRIZIFcIi2wTwICIz+TRMniS09QMv7uwLYT27QfaN56Rs+RvDwIur/6ad8uO/YnzoEpF2c/ZeKBzrLKulK902tq3wqXiqu0hxr05lSyt0njAli+YG6emjEyzlibi6ynNZAyFfttzIu4fg4EvywGlmjq0WsZo7aiOMvaWLYD8qFjC230+kMkJm0yJ+k7wHgFi1XRMCj1egUxzXs7E5wLWA0eWNG7UN6Y+SqMaoTCnVRfUcqQIs23Rgay7v39OI4Z0wNBAeTBW+JI3w5BmMcgVmqPtcpLunSVKIuuz1jg0R+fmZawV9Wi4hgOm4J+3abzJhei8etlZF" - }, - "expected": { - "device_id": "device_browser_recovery", - "local_workspace_id": "workspace_local_recovery", - "workspace_id": "workspace_remote_recovery", - "revision": "1", - "primary_object_id": "object_recovery_one", - "object_keys": [ - { - "object_id": "object_recovery_one", - "epoch": 1, - "key": "ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE=" - }, - { - "object_id": "object_recovery_two", - "epoch": 2, - "key": "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=" - } - ] - } + "format": "noura.browser-recovery-kit", + "version": 1, + "test_only": true, + "note": "Test-only browser recovery-kit fixture. Passphrases and keys are public test vectors; never use them.", + "recovery_passphrase": "browser-recovery-test-passphrase", + "device_passphrase": "browser-device-test-passphrase", + "kit": { + "format": "noura.browser-recovery-kit", + "version": 1, + "createdAt": "2026-01-01T00:00:00.000Z", + "kdf": "pbkdf2-sha256", + "iterations": 310000, + "salt": "LC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSks=", + "nonce": "TE1OT1BRUlNUVVZX", + "ciphertext": "otBwbZTU3dduyDkPxA7ctNRl2Pzi3FEoekjdEAhf21VsdOse7TjpSpOTtJqQWtspBi3mDjyEt0ebFAWWgto4Kao5FOADTt4a3enZ0E93SDz9c3WyzSL4JkE+tDnIlysGSV8x3XYfDII4T/dEcoEkeVrPM7oHma0E8A0VI9XmTiicwXQlMQTzCpoFbQrehEti1zoArMxb8BhPF8DlxzrpFcD31bNcwKuekXe2DpNCMrNF9TtOLxkJ1TLvUHu7PQr7diK+nUxgDWQmlkmRjiC3iWlZQFcKAvfEFZg9dqKjZ7Ubyji9bS5/lYzvHUW9bZRlkEWArfITYhANur4LzKGqKkqhyXLut8fBkw+EDdABnBvhK7a7O8GP1nEOEgmfZW8zpzkqc54Zl3DC6E7TUlVkALMb4r+5ZE6szjZkVLimc+9Eg3oTwdGt+1cAjlXyFSQ7s9GFOBHF2bRVTawh2M2+42zNzR36HhWdUCy09PePnHGwzMcttEiiShYorhnjoNmYf1yPqCZtYxeuUC44y5NW5SfTQstrc+OjOXmj94j9+pInPL5uklLA911Kh04F9iqCSYTyR/kBR+ejxMlyHySup2Xypvfl2DQ+ni7MvPQtuHc8geL/JtvIeNnejTn1C4xVj1ok6xoyEkgsC/9i+COKwPxLFZrmRl6vcR4JYkrTZkGsp1nfi2b/BAACcwWQr0qPljHc6p29/TB/3wnAdASyG+pZady6XeEM3QbgL/XdiZGvcEcd9WcTxpAcaNsTYtKn5puWlvXSCicOejXphQgZkw+zIzmWX36jh1QOznOdGjKklCFJX3eapRJASbdlacFLGyBE3hA4xgSDuukt8CKsu0FFWO40qv9kl3dl2+ZD3UsIjcMHcrvvhT4zIUWIsJgRQql8y8QfVphY8jn8vMnQuSnr5FfDe0zt427j/JYdpEcfXzK8u0DfeTt0XlfxLLpp2+RsIvsqAKTNY/3iNf2QEJYt+0H0bz4rXK38+ytliEKjNNrpbmLA3SdgRGgo8IHnPgC8FH/8L7Ll64ZDrA+AZD3juL3+vfYsWuZoijb0BtdWoCIkeXXOGPOlEskboUbL48x+yVEsN2vzvDKs2aRo4OSOTbpzVBMC0XvZB8ZLWufa2UqhYgVVrglF9VJCn2K11ZjZXcHFOMD6avUdZexzfSu2ep40Jtp0jll/HPePIgg6erjvFMzXdE8wE9lcLctMfNUKS1N5vjZrSvqxcNlT0R8TSWnXMT+mh8/Ui5EBFDkyfKK/Z8wgyuPFnuCiu2Je/R0ThyuWk1HTPtqXlKvqFQPA/GFhkDnV0kSMDJlkknJ84orsuSkv2C7bypD040BJvHT6fQbDjSQw2u88aCvKCOvBZfJXtqq2LRMOwcY9NAHlQj0QQ3kjC/kpGCOfJH89VPbSZaGeaPFftvoKwX158e51Z4N0AHWiRTZ5xXG3Tzexdf4QrC1nQX5UGtRblZjJxCfGQX7mUqkuFX8LXun8AS2YjTOhpaAx2RNuZN0D0YlOLN50blt6dF+GWovxVb6WahA/dEV1UhaGrb3FazVkhqKgcJ5PQLuneV71e0DXIrQaQki5X+jIcRRuF/MNjJDjNRmdfjl/V5ILVhGBYxHXudlEavhz9uoDBFTPSu0N77GlGHhGsE+t/a7TdSLvq75tiQnJ7jcMwyGf1U8OKXTx8I+F3nRgECHRMEU/3jCvPdcnEQE3y2z7sS1Nb38TyJKv1sdZUk2iyQFpYxdc/H7BWBWvcvQU+mOLgD1Y5BKtuYtupUtlYHRNnVjn8JKkNRmLLoEF9uqjO/gouVQaEKjO6D2nHFx6cYvlZX4/XjBfACj8zA0CPh7iUvCTiKkGfiYQRhwj9DT5X7WX5vNvrOCyXxFZCItbb6s2yXgQxe4yqSiquDXltUS7kUlAarGzEt0P5LqzYKmL6n9zS/DOdWPehZnRLK/Vj8kY0g8r1DA4WMEehilFbl/AfccHzHpxJ8hWF/pU10rqLXbCCYIFF4xOnN6aIztgRbZtIVyQQ6lWBp0Zmz6MW+GxOyhHyFVR7YAVdpGABBOHGMQPlvQWOdHMZ9PfAsKLugmoE8su75Eqd+NU6Pw+YLdkqPMd87cRZYa21/yxxbLx0ooOHfd382ZQZ7gQH0uwXLtsMqRL72jSt6u4vF6l1UU6FrrfDZqGpNK+whIGlPoDabUlyq0wD/xhaaI6KVGzzJA7O5Kudf20rE4nt+GyWZK1RddIJJTfqRnnegvjxN+mzbaz9H6QoWprF0iyfjugJ+ZJIDB0tbVwhg+btW3O/N6C3YinfqtBshbnKjQtqlLCZBmwHyyACjrCXdLZa5Hn9LsqtPOEjrFE56rqE4k2rNM5Bwq7yGSqFYVrSKidxOcyfy51LN8r8MNv07m6r1rrVCDY6FzQeKq7AUfT6cggMz4yUXBRK7S2VnEvREl5cyai7Ui8mYtLSqjiHWiszMAmHXo7kUfBXTlZiJyI03uaftqmNOrpIyTqkF44z01IHRIaqaJLcQ9mbRv3e3/MaUDk9hU+e7adIV44eEqyWUmKpKzdo/i5QC02B2Hbj5tTlYAke7Nuf5476RTEhTb2dPL5xu1LUdE4wcGzetu3zIc3MUDztYyOFRXlBtKbge/LbLSXDkCYW6z3cHmiucYn8e6ELTACCS3mVOBiAUeGNLgHXB8Cm7N9husalD+bZ0meIUMHGQxDwusB6wbBkTC2pGJ7iNSW0jgTrhek7g57B1Vj3soJNvYtRIZIFcIi2wTwICIz+TRMniS09QMv7uwLYT27QfaN56Rs+RvDwIur/6ad8uO/YnzoEpF2c/ZeKBzrLKulK902tq3wqXiqu0hxr05lSyt0njAli+YG6emjEyzlibi6ynNZAyFfttzIu4fg4EvywGlmjq0WsZo7aiOMvaWLYD8qFjC230+kMkJm0yJ+k7wHgFi1XRMCj1egUxzXs7E5wLWA0eWNG7UN6Y+SqMaoTCnVRfUcqQIs23Rgay7v39OI4Z0wNBAeTBW+JI3w5BmMcgVmqPtcpLunSVKIuuz1jg0R+fmZawV9Wi4hgOm4J+3abzJhei8etlZF" + }, + "expected": { + "device_id": "device_browser_recovery", + "local_workspace_id": "workspace_local_recovery", + "workspace_id": "workspace_remote_recovery", + "revision": "1", + "primary_object_id": "object_recovery_one", + "object_keys": [ + { + "object_id": "object_recovery_one", + "epoch": 1, + "key": "ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE=" + }, + { + "object_id": "object_recovery_two", + "epoch": 2, + "key": "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=" + } + ] + } } diff --git a/docs/workspace-format/fixtures/operation-v1.json b/docs/workspace-format/fixtures/operation-v1.json index 132f827..c243831 100644 --- a/docs/workspace-format/fixtures/operation-v1.json +++ b/docs/workspace-format/fixtures/operation-v1.json @@ -1,220 +1,220 @@ { - "invalid": [ - { - "expected_error": "sync_invalid_signature", - "name": "tampered_signature", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "verify", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "AJ5BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_decrypt_failed", - "name": "tampered_ciphertext", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEU=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "vuTrjlgr8TurKmcZV0ShqH+rLLPR1vxizzQ8wXXZN5PlKRiQLK7o3pjLHlhddbjrHRNsXNBGBY2MuQXRi0aWDA==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_decrypt_failed", - "name": "tampered_metadata", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object_other", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "aWHplydHZeEijxFXT7ct13Tq2yjDbSVnLa/tmTnoNZYVstLpNKSwbsmb2XfyFFIhhbba0A3pwOYh02LrfWNKAw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_decrypt_failed", - "name": "wrong_object_key", - "object_key": "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_invalid_signature", - "name": "wrong_signer_public", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "verify", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "E5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8o=" - }, - { - "expected_error": "sync_invalid_base64", - "name": "bad_base64_nonce", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "not-base64!!", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_invalid_envelope", - "name": "epoch_zero", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 0, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "V47O83fjtS1X2S4P8aiCl/7fb5Jzk3JQB4VoJeIrdcHXOZUoj7QKRscW5yUf7zVNhhwHEe46LHZ5WEslLylKBw==", - "version": 1, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - }, - { - "expected_error": "sync_invalid_envelope", - "name": "unsupported_version", - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation": "open", - "operation_value": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "JzbTyy/+rFWeYsuMiDB1cOtSPGfGnLuixyRr9h9V5E1KhCMs0+qZhG9b91ZcEV1pnRMmQcFOuvUQjF1pazyOAQ==", - "version": 3, - "workspaceId": "workspace" - }, - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" - } - ], - "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", - "operation_domain": "noura.sync.operation", - "payload_domain": "noura.sync.payload", - "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", - "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", - "valid": [ - { - "expected_operation": { - "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", - "deviceId": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "objectId": "object", - "operationId": "operation_basic", - "policyRevision": "1", - "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", - "version": 1, - "workspaceId": "workspace" - }, - "expected_plaintext_base64": "cHJpdmF0ZS9wYXRoLm1kCm9wZXJhdGlvbiBib2R5Cg==", - "inputs": { - "device_id": "device_browser", - "epoch": 1, - "nonce": "BgYGBgYGBgYGBgYG", - "object_id": "object", - "operation_id": "operation_basic", - "plaintext_base64": "cHJpdmF0ZS9wYXRoLm1kCm9wZXJhdGlvbiBib2R5Cg==", - "policy_revision": "1", - "workspace_id": "workspace" - }, - "name": "operation-v1-basic" - }, - { - "expected_operation": { - "ciphertext": "cEKn1nkN9NEa+R6NQmTFgSeRDaWPrJNrQAIilPbXAS7XWn5CM8XeYt0=", - "deviceId": "device_browser", - "epoch": 1, - "generation": "generation", - "kind": "text", - "nonce": "BwcHBwcHBwcHBwcH", - "objectId": "object", - "operationId": "operation_document", - "policyRevision": "1", - "signature": "w8a7Ggr7TdYqnRyeU3+qhXooxxRvgqCb5ZSGHTadmOOhR0TEAIueplDvQjuTNJGdvzQ7ay7gaUih963//bIzBg==", - "version": 2, - "workspaceId": "workspace" - }, - "expected_plaintext_base64": "ZG9jdW1lbnQgZ2VuZXJhdGlvbiB0ZXh0Cg==", - "inputs": { - "device_id": "device_browser", - "epoch": 1, - "generation": "generation", - "kind": "text", - "nonce": "BwcHBwcHBwcHBwcH", - "object_id": "object", - "operation_id": "operation_document", - "plaintext_base64": "ZG9jdW1lbnQgZ2VuZXJhdGlvbiB0ZXh0Cg==", - "policy_revision": "1", - "workspace_id": "workspace" - }, - "name": "operation-v2-document" - } - ], - "version": 1 + "invalid": [ + { + "expected_error": "sync_invalid_signature", + "name": "tampered_signature", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "verify", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "AJ5BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_decrypt_failed", + "name": "tampered_ciphertext", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEU=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "vuTrjlgr8TurKmcZV0ShqH+rLLPR1vxizzQ8wXXZN5PlKRiQLK7o3pjLHlhddbjrHRNsXNBGBY2MuQXRi0aWDA==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_decrypt_failed", + "name": "tampered_metadata", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object_other", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "aWHplydHZeEijxFXT7ct13Tq2yjDbSVnLa/tmTnoNZYVstLpNKSwbsmb2XfyFFIhhbba0A3pwOYh02LrfWNKAw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_decrypt_failed", + "name": "wrong_object_key", + "object_key": "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_invalid_signature", + "name": "wrong_signer_public", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "verify", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "E5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8o=" + }, + { + "expected_error": "sync_invalid_base64", + "name": "bad_base64_nonce", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "not-base64!!", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_invalid_envelope", + "name": "epoch_zero", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 0, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "V47O83fjtS1X2S4P8aiCl/7fb5Jzk3JQB4VoJeIrdcHXOZUoj7QKRscW5yUf7zVNhhwHEe46LHZ5WEslLylKBw==", + "version": 1, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + }, + { + "expected_error": "sync_invalid_envelope", + "name": "unsupported_version", + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation": "open", + "operation_value": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "JzbTyy/+rFWeYsuMiDB1cOtSPGfGnLuixyRr9h9V5E1KhCMs0+qZhG9b91ZcEV1pnRMmQcFOuvUQjF1pazyOAQ==", + "version": 3, + "workspaceId": "workspace" + }, + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=" + } + ], + "object_key": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", + "operation_domain": "noura.sync.operation", + "payload_domain": "noura.sync.payload", + "signing_public": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=", + "signing_secret": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + "valid": [ + { + "expected_operation": { + "ciphertext": "20SaLZi3b2+wn4Vpg+0U3xsJAI8WRC2HtMKsrt19qTRU4mUkfN+9eX1KL/t/UEQ=", + "deviceId": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "objectId": "object", + "operationId": "operation_basic", + "policyRevision": "1", + "signature": "/55BnFGptO77WBUFYhRVLbEe4SDaAerJHNCFtSRxUPosevrGp72/kcOsiOyA3zKifZdhueoMv29Ta+KpYePGAw==", + "version": 1, + "workspaceId": "workspace" + }, + "expected_plaintext_base64": "cHJpdmF0ZS9wYXRoLm1kCm9wZXJhdGlvbiBib2R5Cg==", + "inputs": { + "device_id": "device_browser", + "epoch": 1, + "nonce": "BgYGBgYGBgYGBgYG", + "object_id": "object", + "operation_id": "operation_basic", + "plaintext_base64": "cHJpdmF0ZS9wYXRoLm1kCm9wZXJhdGlvbiBib2R5Cg==", + "policy_revision": "1", + "workspace_id": "workspace" + }, + "name": "operation-v1-basic" + }, + { + "expected_operation": { + "ciphertext": "cEKn1nkN9NEa+R6NQmTFgSeRDaWPrJNrQAIilPbXAS7XWn5CM8XeYt0=", + "deviceId": "device_browser", + "epoch": 1, + "generation": "generation", + "kind": "text", + "nonce": "BwcHBwcHBwcHBwcH", + "objectId": "object", + "operationId": "operation_document", + "policyRevision": "1", + "signature": "w8a7Ggr7TdYqnRyeU3+qhXooxxRvgqCb5ZSGHTadmOOhR0TEAIueplDvQjuTNJGdvzQ7ay7gaUih963//bIzBg==", + "version": 2, + "workspaceId": "workspace" + }, + "expected_plaintext_base64": "ZG9jdW1lbnQgZ2VuZXJhdGlvbiB0ZXh0Cg==", + "inputs": { + "device_id": "device_browser", + "epoch": 1, + "generation": "generation", + "kind": "text", + "nonce": "BwcHBwcHBwcHBwcH", + "object_id": "object", + "operation_id": "operation_document", + "plaintext_base64": "ZG9jdW1lbnQgZ2VuZXJhdGlvbiB0ZXh0Cg==", + "policy_revision": "1", + "workspace_id": "workspace" + }, + "name": "operation-v2-document" + } + ], + "version": 1 } diff --git a/docs/workspace-format/fixtures/policy-web-v1.json b/docs/workspace-format/fixtures/policy-web-v1.json index 4cebf03..68cefb2 100644 --- a/docs/workspace-format/fixtures/policy-web-v1.json +++ b/docs/workspace-format/fixtures/policy-web-v1.json @@ -1,96 +1,96 @@ { - "domain": "noura.sync.access", - "info": "noura.sync.access.v1", - "version": 1, - "policy": { - "version": 1, - "workspaceId": "workspace", - "revision": "1", - "previousPolicyDigest": null, - "deviceId": "device_signer", - "members": [ - { - "accountId": "account_editor", - "role": "editor" - }, - { - "accountId": "account_owner", - "role": "owner" - } - ], - "objects": [ - { - "objectId": "object", - "epoch": 1, - "grants": [ - { - "accountId": "account_editor", - "role": "editor" - } - ], - "envelopes": [ - { - "deviceId": "device_browser", - "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "construction": "web", - "recipientPublicKey": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "ephemeralPublicKey": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "nonce": "BgYGBgYGBgYGBgYG" - } - ] - } - ] - }, - "signing_bytes": "[\"noura.sync.access\",1,\"workspace\",\"1\",null,\"device_signer\",[[\"account_editor\",\"editor\"],[\"account_owner\",\"owner\"]],[[\"object\",1,[[\"account_editor\",\"editor\"]],[[\"device_browser\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\",\"web\",\"zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=\",\"rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=\",\"BgYGBgYGBgYGBgYG\"]]]]]", - "mixed": { - "policy": { - "version": 1, - "workspaceId": "workspace", - "revision": "1", - "previousPolicyDigest": null, - "deviceId": "device_signer", - "members": [ - { - "accountId": "account_editor", - "role": "editor" - }, - { - "accountId": "account_owner", - "role": "owner" - } - ], - "objects": [ - { - "objectId": "object", - "epoch": 1, - "grants": [ - { - "accountId": "account_editor", - "role": "editor" - } - ], - "envelopes": [ - { - "deviceId": "device_age", - "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==" - }, - { - "deviceId": "device_browser", - "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", - "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", - "construction": "web", - "recipientPublicKey": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", - "ephemeralPublicKey": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", - "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", - "nonce": "BgYGBgYGBgYGBgYG" - } - ] - } - ] - }, - "signing_bytes": "[\"noura.sync.access\",1,\"workspace\",\"1\",null,\"device_signer\",[[\"account_editor\",\"editor\"],[\"account_owner\",\"owner\"]],[[\"object\",1,[[\"account_editor\",\"editor\"]],[[\"device_age\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\"],[\"device_browser\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\",\"web\",\"zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=\",\"rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=\",\"BgYGBgYGBgYGBgYG\"]]]]]" - } + "domain": "noura.sync.access", + "info": "noura.sync.access.v1", + "version": 1, + "policy": { + "version": 1, + "workspaceId": "workspace", + "revision": "1", + "previousPolicyDigest": null, + "deviceId": "device_signer", + "members": [ + { + "accountId": "account_editor", + "role": "editor" + }, + { + "accountId": "account_owner", + "role": "owner" + } + ], + "objects": [ + { + "objectId": "object", + "epoch": 1, + "grants": [ + { + "accountId": "account_editor", + "role": "editor" + } + ], + "envelopes": [ + { + "deviceId": "device_browser", + "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "construction": "web", + "recipientPublicKey": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "ephemeralPublicKey": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "nonce": "BgYGBgYGBgYGBgYG" + } + ] + } + ] + }, + "signing_bytes": "[\"noura.sync.access\",1,\"workspace\",\"1\",null,\"device_signer\",[[\"account_editor\",\"editor\"],[\"account_owner\",\"owner\"]],[[\"object\",1,[[\"account_editor\",\"editor\"]],[[\"device_browser\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\",\"web\",\"zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=\",\"rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=\",\"BgYGBgYGBgYGBgYG\"]]]]]", + "mixed": { + "policy": { + "version": 1, + "workspaceId": "workspace", + "revision": "1", + "previousPolicyDigest": null, + "deviceId": "device_signer", + "members": [ + { + "accountId": "account_editor", + "role": "editor" + }, + { + "accountId": "account_owner", + "role": "owner" + } + ], + "objects": [ + { + "objectId": "object", + "epoch": 1, + "grants": [ + { + "accountId": "account_editor", + "role": "editor" + } + ], + "envelopes": [ + { + "deviceId": "device_age", + "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==" + }, + { + "deviceId": "device_browser", + "wrappedKey": "evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=", + "signature": "nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==", + "construction": "web", + "recipientPublicKey": "zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=", + "ephemeralPublicKey": "rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=", + "salt": "BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=", + "nonce": "BgYGBgYGBgYGBgYG" + } + ] + } + ] + }, + "signing_bytes": "[\"noura.sync.access\",1,\"workspace\",\"1\",null,\"device_signer\",[[\"account_editor\",\"editor\"],[\"account_owner\",\"owner\"]],[[\"object\",1,[[\"account_editor\",\"editor\"]],[[\"device_age\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\"],[\"device_browser\",\"evwIN2h9DkuH6a/SkKGHT6fqGMpAF0hRDgqGQcQBdA5cL9mEblvrLga7/VVhvPsbyyNK2yY/HGN3yzEn7Un0AJd7EXYTmH0qcsl4Yz3aT5QGQwaCrIHfrt5q4YOhKVSkXO0IrrUPvOP5IfVjICpfPcY=\",\"nlHrI6iEs1pjjRl7yAXgsQjzOjMb57Zf6zy1CMfIcsVPycaJRRExm8Gt5l4/rzCpq1KdiDWieBnoFgzjaj4CBQ==\",\"web\",\"zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=\",\"rAGyIJ6GNU+4UyN7XeD0+rE8f8v0M6YcAZNpYX/s8Qs=\",\"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=\",\"BgYGBgYGBgYGBgYG\"]]]]]" + } } diff --git a/docs/workspace-format/sync-v1.md b/docs/workspace-format/sync-v1.md index 78b487e..4e0bd03 100644 --- a/docs/workspace-format/sync-v1.md +++ b/docs/workspace-format/sync-v1.md @@ -90,7 +90,7 @@ Local resolution retains its signed ciphertext intent under `resolutions/` befor The shared conformance fixtures cover all three payload versions. Rust remains the serializer; TypeScript exposes validation only. -Directory fsync is implemented on Unix. Equivalent crash-durability behavior on Windows still needs implementation and platform testing. Move/identity conflict resolution and collaborative text updates remain unfinished. +Directory fsync is implemented on Unix. Equivalent crash-durability behavior on Windows still needs implementation and platform testing. Move/identity conflict resolution remains unfinished. Collaborative text updates are experimental, disabled by default, and remain release-gated; see [the collaboration protocol](collaboration.md). ## Encrypted attachments diff --git a/docs/workspace-format/v1.md b/docs/workspace-format/v1.md index a9a24e5..7c10f6b 100644 --- a/docs/workspace-format/v1.md +++ b/docs/workspace-format/v1.md @@ -28,7 +28,7 @@ The workspace ID identifies the workspace across moves. Device-local indexes and - Plugin identifiers are a lowercase letter followed by lowercase letters, digits, or hyphens, at most 64 characters total. - Order is not significant, and writers deduplicate the list. Readers tolerate a hand edit's duplicates and unsorted entries by canonicalizing on read; writers serialize the deduplicated list in sorted order. - An empty list is valid and enables no domain modules. -- Unknown identifiers are preserved and tolerated so future ecosystem plugins do not break older builds; readers ignore ids they do not implement. +- Readers preserve unknown identifiers and ignore IDs they do not implement, so future ecosystem plugins do not break older builds. - Application plugin toggles rewrite this file atomically and refresh `updated`. ## Managed Markdown