Public links: guest editing of notes and text files (Edit permission) #105

Closed
opened 2026-09-25 17:30:21 +00:00 by kayg · 24 comments
Owner

Context

DESIGN §22 decides that a public link can have an Edit permission: a visitor opens a shared note or text file in the editor as a named guest and changes it. The share audit (branch job/share-audit, 2026-09-25) found that this was never built:

  • There is no public endpoint to read and write a note or text file as a guest, and no guest editor on the link page (apps/web/src/lib/files/PublicLinkPage.svelte).
  • The only effect of edit: true was in the file drop: with upload and edit, a visitor could send a tus upload with Upload-Conflict-Policy: replace and overwrite any file in the shared folder (photos, PDFs, executables), not only notes and text. That was a security hole.

The audit closed the hole and removed the option until it is built:

  • POST/PUT /api/v1/files/links returns 400 for permissions.edit = true ("editing and linked notes are not available for public links yet").
  • A stored link with edit: true gets nothing from it (public.rs authorize clears the flag); a public upload never replaces a file, at creation and at completion (uploads.rs finish).
  • The Share dialog has no Edit toggle.

Work

Build guest editing for public links:

  1. Decide the guest identity (a name the visitor types, shown to the owner in the change history and as the collab cursor).
  2. Server: a public collab entry point for one Note or text file inside the link, scoped to the link's item (item ID, not path), refused for everything else (binary files, files outside the link, folder metadata, .calternal/). Every change is a normal versioned write (a version is kept); the owner sees who changed it.
  3. Rate limits and size limits per link and per IP; password and expiry enforced on the socket, not only on the page.
  4. Web: the link page opens the note in the shared editor for a visitor, with the guest name.
  5. Re-enable edit in validate() in crates/plugins/files/src/public.rs and the toggle in ShareDialog.svelte (disabled with a reason for a file that is not a note or text).
  6. Tests: extend tests/adversarial/attack2.py section share-options (edit only changes notes/text, only inside the shared item, never binary files, never after expiry or revoke) and apps/web/e2e/share.mjs.
## Context DESIGN §22 decides that a public link can have an **Edit** permission: a visitor opens a shared note or text file in the editor as a named guest and changes it. The share audit (branch `job/share-audit`, 2026-09-25) found that this was never built: - There is no public endpoint to read and write a note or text file as a guest, and no guest editor on the link page (`apps/web/src/lib/files/PublicLinkPage.svelte`). - The only effect of `edit: true` was in the file drop: with `upload` and `edit`, a visitor could send a tus upload with `Upload-Conflict-Policy: replace` and overwrite **any** file in the shared folder (photos, PDFs, executables), not only notes and text. That was a security hole. The audit closed the hole and removed the option until it is built: - `POST/PUT /api/v1/files/links` returns 400 for `permissions.edit = true` ("editing and linked notes are not available for public links yet"). - A stored link with `edit: true` gets nothing from it (`public.rs` `authorize` clears the flag); a public upload never replaces a file, at creation and at completion (`uploads.rs` `finish`). - The Share dialog has no Edit toggle. ## Work Build guest editing for public links: 1. Decide the guest identity (a name the visitor types, shown to the owner in the change history and as the collab cursor). 2. Server: a public collab entry point for one Note or text file inside the link, scoped to the link's item (item ID, not path), refused for everything else (binary files, files outside the link, folder metadata, `.calternal/`). Every change is a normal versioned write (a version is kept); the owner sees who changed it. 3. Rate limits and size limits per link and per IP; password and expiry enforced on the socket, not only on the page. 4. Web: the link page opens the note in the shared editor for a visitor, with the guest name. 5. Re-enable `edit` in `validate()` in `crates/plugins/files/src/public.rs` and the toggle in `ShareDialog.svelte` (disabled with a reason for a file that is not a note or text). 6. Tests: extend `tests/adversarial/attack2.py` section `share-options` (edit only changes notes/text, only inside the shared item, never binary files, never after expiry or revoke) and `apps/web/e2e/share.mjs`.
Author
Owner

Starting work on job/share-edit, based on dev at 44b15bec4f. I am implementing named guest editing for supported Note/text items and linked notes under the public-link scope, with adversarial coverage.

Starting work on job/share-edit, based on dev at 44b15bec4f4e78e788c85b7ec60d54892a293b84. I am implementing named guest editing for supported Note/text items and linked notes under the public-link scope, with adversarial coverage.
Author
Owner

Finding during focused Files tests: public_link_options_that_do_not_fit_are_refused and the file-drop test showed that an Edit request for a folder returned HTTP 409 because content validation tried to read the folder before option validation. The create handler now rejects unsupported Edit targets before reading file content; the expected result is HTTP 400. I am rerunning the focused tests.

Finding during focused Files tests: `public_link_options_that_do_not_fit_are_refused` and the file-drop test showed that an Edit request for a folder returned HTTP 409 because content validation tried to read the folder before option validation. The create handler now rejects unsupported Edit targets before reading file content; the expected result is HTTP 400. I am rerunning the focused tests.
Author
Owner

Decision for the details that DESIGN §22 left open: public edit sessions last eight hours. The token travels in the WebSocket subprotocol and the HTTP response is Cache-Control: no-store. REST Edit requests are limited to 60 per verified IP and 600 per link per minute; live frames are limited to 300 per verified IP and link per minute. Note content is capped at 2 MiB, with a 3 MiB WebSocket frame cap for Yjs overhead. These limits keep an editor session usable while bounding unauthenticated work.

Decision for the details that DESIGN §22 left open: public edit sessions last eight hours. The token travels in the WebSocket subprotocol and the HTTP response is `Cache-Control: no-store`. REST Edit requests are limited to 60 per verified IP and 600 per link per minute; live frames are limited to 300 per verified IP and link per minute. Note content is capped at 2 MiB, with a 3 MiB WebSocket frame cap for Yjs overhead. These limits keep an editor session usable while bounding unauthenticated work.
Author
Owner

Finding from the production E2E: a public Edit link to a Note did not expose the expected Guest collaboration heading. GET /api/v1/public/{slug} currently uses the storage basename as its title; Notes filenames include the date and stable ID suffix, while the indexed Notes title is the user-facing title. I am updating public link metadata to use the indexed title for an explicitly shared Notes item, while keeping hidden names masked.

Finding from the production E2E: a public Edit link to a Note did not expose the expected `Guest collaboration` heading. `GET /api/v1/public/{slug}` currently uses the storage basename as its title; Notes filenames include the date and stable ID suffix, while the indexed Notes title is the user-facing title. I am updating public link metadata to use the indexed title for an explicitly shared Notes item, while keeping hidden names masked.
Author
Owner

Finding from the production E2E: a public guest's live Note edit reaches the owner-visible Note body, but GET /api/v1/files/versions?path=<Note> did not show any Version with changed_by: "Public guest" after 15 seconds of polling. I am tracing the WebSocket save callback and Files item identity path; the REST text-file Edit attribution test already passes.

Finding from the production E2E: a public guest's live Note edit reaches the owner-visible Note body, but `GET /api/v1/files/versions?path=<Note>` did not show any Version with `changed_by: "Public guest"` after 15 seconds of polling. I am tracing the WebSocket save callback and Files item identity path; the REST text-file Edit attribution test already passes.
Author
Owner

Follow-up evidence: the live Note edit changed the Files item ID from daeb6df3-1449-46bd-9dfd-56802bc78cf2 to f516c35d-5c3b-4464-985c-ee58464f975e. The Root change bridge can index the post-write fingerprint before the Notes completion hook runs; record_note_write then treated the already-adopted fingerprint as a replacement and rotated the ID. This also explains the missing guest Version attribution because that record is keyed by the original item ID. I am fixing the race and adding a regression case.

Follow-up evidence: the live Note edit changed the Files item ID from `daeb6df3-1449-46bd-9dfd-56802bc78cf2` to `f516c35d-5c3b-4464-985c-ee58464f975e`. The Root change bridge can index the post-write fingerprint before the Notes completion hook runs; `record_note_write` then treated the already-adopted fingerprint as a replacement and rotated the ID. This also explains the missing guest Version attribution because that record is keyed by the original item ID. I am fixing the race and adding a regression case.
Author
Owner

The Files item ID now stays stable across the live Note write, but the E2E still shows the saved Version without changed_by. The server emitted no attribution-write error. Code inspection found that the collab flush snapshots the Yjs document before it reads pending_public, while the frame handler sets pending_public only after releasing the document lock. A flush can capture a guest edit without its guest name. I am synchronizing those snapshots and adding the end-to-end regression check.

The Files item ID now stays stable across the live Note write, but the E2E still shows the saved Version without `changed_by`. The server emitted no attribution-write error. Code inspection found that the collab flush snapshots the Yjs document before it reads `pending_public`, while the frame handler sets `pending_public` only after releasing the document lock. A flush can capture a guest edit without its guest name. I am synchronizing those snapshots and adding the end-to-end regression check.
Author
Owner

The production share E2E passed the guest Note live edit, stable Files item identity, named caret, and Version attribution assertions. The later auto-plan check timed out while waiting for preview text; server logs showed SQL pool acquisitions between 2.1 and 3.6 seconds. I reproduced that case alone against the same production build: public link creation returned 200, /api/v1/public/auto-plan returned 200, /preview returned 200, and the document contained The root plan. This points to shared-host load during the full run; I am rerunning it after the adversarial round.

The production share E2E passed the guest Note live edit, stable Files item identity, named caret, and Version attribution assertions. The later `auto-plan` check timed out while waiting for preview text; server logs showed SQL pool acquisitions between 2.1 and 3.6 seconds. I reproduced that case alone against the same production build: public link creation returned 200, `/api/v1/public/auto-plan` returned 200, `/preview` returned 200, and the document contained `The root plan.` This points to shared-host load during the full run; I am rerunning it after the adversarial round.
Author
Owner

Adversarial round finding during concurrent host load: the appearance concurrency probe reported SLOW for requests 13 and 16–31 (5.3–10.6 seconds); each returned HTTP 200. Several other worktrees were compiling at the time. This is a SLOW-only load result under the owner rule. The share-options probe is still running; I will report any non-SLOW findings separately.

Adversarial round finding during concurrent host load: the appearance concurrency probe reported `SLOW` for requests 13 and 16–31 (5.3–10.6 seconds); each returned HTTP 200. Several other worktrees were compiling at the time. This is a SLOW-only load result under the owner rule. The share-options probe is still running; I will report any non-SLOW findings separately.
Author
Owner

Follow-up from the same adversarial round: reminders incremental sync reported SLOW 10.3s status 207. This is not a share route and ran while multiple worktrees were compiling. It is a SLOW-only load result under the owner rule; the public-link probe remains pending.

Follow-up from the same adversarial round: reminders incremental sync reported `SLOW 10.3s status 207`. This is not a share route and ran while multiple worktrees were compiling. It is a SLOW-only load result under the owner rule; the public-link probe remains pending.
Author
Owner

The broad adversarial round also reported Photos search limit too large: SLOW 5.7s status 400. All 19 findings from this phase are SLOW response-time findings; none reported a 5xx, accepted hostile input, or unexpected status. The HEIF/AVIF upload probe passed. Its optional thumbnail-header check was skipped because no thumbnail appeared within 10 seconds (the probe says the media worker may be busy or missing).

The broad adversarial round also reported `Photos search limit too large: SLOW 5.7s status 400`. All 19 findings from this phase are SLOW response-time findings; none reported a 5xx, accepted hostile input, or unexpected status. The HEIF/AVIF upload probe passed. Its optional thumbnail-header check was skipped because no thumbnail appeared within 10 seconds (the probe says the media worker may be busy or missing).
Author
Owner

Adversarial share-options finding: the shared photo had no thumbnail after 30 seconds; public thumbnail requests for sizes 256 and 1024 returned 404 application/json. The preceding media upload probe also found no thumbnail after 10 seconds and skipped its header check. The probe used the server launched by tests/adversarial/run.sh; I am checking media sandbox availability before deciding whether this is a code defect or a test-launch gap.

Adversarial `share-options` finding: the shared photo had no thumbnail after 30 seconds; public thumbnail requests for sizes 256 and 1024 returned `404 application/json`. The preceding media upload probe also found no thumbnail after 10 seconds and skipped its header check. The probe used the server launched by `tests/adversarial/run.sh`; I am checking media sandbox availability before deciding whether this is a code defect or a test-launch gap.
Author
Owner

Follow-up: the real-server share-options section finished with 15 findings. Every listed finding was caused by the missing photo thumbnail: the photo setup timed out, and public thumbnail requests returned 404 (including cache revalidation and download-limit checks). The public Edit and Linked Notes checks reported no additional findings. The local host has no calternal-media-sandbox command on PATH; the Files media code invokes that fixed command, and deploy/Containerfile.runtime installs it. I am checking the available runtime image to verify whether this is a test-launch gap.

Follow-up: the real-server `share-options` section finished with 15 findings. Every listed finding was caused by the missing photo thumbnail: the photo setup timed out, and public thumbnail requests returned 404 (including cache revalidation and download-limit checks). The public Edit and Linked Notes checks reported no additional findings. The local host has no `calternal-media-sandbox` command on `PATH`; the Files media code invokes that fixed command, and `deploy/Containerfile.runtime` installs it. I am checking the available runtime image to verify whether this is a test-launch gap.
Author
Owner

Root cause confirmed: the server passed [descriptor=0] to vipsheader, but the pinned vips 8.16.1 CLI treats that as an empty filename. The existing real-media test failed with NotFound. I changed the header probe to read /proc/self/fd/0, which names the inherited open file handle and does not resolve a Home path. With an isolated local media runner, the regression test now passes for all six image/video fixtures and generates WebP thumbnails.

Root cause confirmed: the server passed `[descriptor=0]` to `vipsheader`, but the pinned vips 8.16.1 CLI treats that as an empty filename. The existing real-media test failed with `NotFound`. I changed the header probe to read `/proc/self/fd/0`, which names the inherited open file handle and does not resolve a Home path. With an isolated local media runner, the regression test now passes for all six image/video fixtures and generates WebP thumbnails.
Author
Owner

Production E2E follow-up: I reran with the local media runner available and temporary save-path diagnostics. The live guest Note edit returned a Files Version and completed the Public guest attribution callback; the guest Version assertion passed. The run later timed out waiting for The root plan. on the unrelated auto-plan link (30 seconds). At that time multiple worktrees were building Vite/Rust targets. This matches the earlier isolated successful auto-plan reproduction and is a shared-host SLOW/load result, not a new share route failure. I removed the temporary diagnostics; no source changes remain from this check.

Production E2E follow-up: I reran with the local media runner available and temporary save-path diagnostics. The live guest Note edit returned a Files Version and completed the `Public guest` attribution callback; the guest Version assertion passed. The run later timed out waiting for `The root plan.` on the unrelated `auto-plan` link (30 seconds). At that time multiple worktrees were building Vite/Rust targets. This matches the earlier isolated successful `auto-plan` reproduction and is a shared-host SLOW/load result, not a new share route failure. I removed the temporary diagnostics; no source changes remain from this check.
Author
Owner

Correction to the earlier E2E load diagnosis: the repeated auto-plan timeout was a fixture collision, not host load. The production page returned HTTP 200 and displayed Edited by a guest. for root Plan.md. The plan-doc test had opened the root Plan.md, edited it, then the later auto-plan assertion expected its original The root plan. text. I changed plan-doc to share Docs/Plan.md, updated its linked Note and download/Version paths, and kept root Plan.md unchanged for the automatic presentation check. I am rerunning the production E2E now.

Correction to the earlier E2E load diagnosis: the repeated `auto-plan` timeout was a fixture collision, not host load. The production page returned HTTP 200 and displayed `Edited by a guest.` for root `Plan.md`. The `plan-doc` test had opened the root `Plan.md`, edited it, then the later `auto-plan` assertion expected its original `The root plan.` text. I changed `plan-doc` to share `Docs/Plan.md`, updated its linked Note and download/Version paths, and kept root `Plan.md` unchanged for the automatic presentation check. I am rerunning the production E2E now.
Author
Owner

Full-suite finding and fix: cargo test caught missing OpenAPI 429 response declarations for the public Edit read/write and linked Notes list/item routes. Those routes are rate limited. I added the declarations; cargo fmt --check and cargo test -p calternal-server --bin calternal-server openapi_declares_known_rate_limit_responses passed. Fix committed as f732b41b. I am rerunning the full Rust suite with this fix.

Full-suite finding and fix: `cargo test` caught missing OpenAPI 429 response declarations for the public Edit read/write and linked Notes list/item routes. Those routes are rate limited. I added the declarations; `cargo fmt --check` and `cargo test -p calternal-server --bin calternal-server openapi_declares_known_rate_limit_responses` passed. Fix committed as `f732b41b`. I am rerunning the full Rust suite with this fix.
Author
Owner

Finding reproduced on the merged tree: the public Note’s atomic file replacement is visible before the collaboration callback refreshes the Files Index and records the guest Version. During that gap, GET /api/v1/files/versions can re-index the new inode as a new Files item. The guest Version then remains attached to the Share’s prior item ID, so the Versions response omits “Public guest.” The production E2E showed two saved versions with no attribution, and the new regression test failed because the Versions route returned while the mutation lock was held. I am fixing this by keeping the public Note write and its Files callbacks within that lock, and making Versions reads and public guest rechecks wait for the refresh.

Finding reproduced on the merged tree: the public Note’s atomic file replacement is visible before the collaboration callback refreshes the Files Index and records the guest Version. During that gap, GET /api/v1/files/versions can re-index the new inode as a new Files item. The guest Version then remains attached to the Share’s prior item ID, so the Versions response omits “Public guest.” The production E2E showed two saved versions with no attribution, and the new regression test failed because the Versions route returned while the mutation lock was held. I am fixing this by keeping the public Note write and its Files callbacks within that lock, and making Versions reads and public guest rechecks wait for the refresh.
Author
Owner

The post-fix production E2E passed the public Note live edit and guest Version attribution. It then stalled at the expiry fixture because the test gives the link only three seconds before expiry; on the shared host the first visitor page did not render the file list inside that window. I am extending the fixture window and its follow-up delay so the test can confirm both the initial open and later expiry under load.

The post-fix production E2E passed the public Note live edit and guest Version attribution. It then stalled at the expiry fixture because the test gives the link only three seconds before expiry; on the shared host the first visitor page did not render the file list inside that window. I am extending the fixture window and its follow-up delay so the test can confirm both the initial open and later expiry under load.
Author
Owner

The gallery E2E measured a real layout shift: the Linked Notes section moved from y=186 to y=570 when the folder rows loaded, for CLS 0.075 against the 0.05 limit. The page showed Linked Notes while the gallery request was still pending. I now defer that trailing section until the first folder list completes. The rebuilt production E2E passes the gallery CLS check and all share flows.

The gallery E2E measured a real layout shift: the Linked Notes section moved from y=186 to y=570 when the folder rows loaded, for CLS 0.075 against the 0.05 limit. The page showed Linked Notes while the gallery request was still pending. I now defer that trailing section until the first folder list completes. The rebuilt production E2E passes the gallery CLS check and all share flows.
Author
Owner

Coverage finding while resuming #105: share-options refused Edit on folders and upload links, but it did not exercise a binary target or an Edit session after revoke, password rotation, or expiry. The server currently rechecks the link grant on the socket; I added real-server probe cases for those boundaries and will run the probe before final gates.

Coverage finding while resuming #105: `share-options` refused Edit on folders and upload links, but it did not exercise a binary target or an Edit session after revoke, password rotation, or expiry. The server currently rechecks the link grant on the socket; I added real-server probe cases for those boundaries and will run the probe before final gates.
Author
Owner

After git merge dev, the local adversarial harness reported a non-load test failure before share-options: authz_matrix.py raised OpenAPI operation has no authorization policy: GET /api/v1/analytics (analytics_report) at its policy lookup. attack.py also logged one SLOW Note create (7.0 s). I am checking the merged Analytics route policy and will close the matrix gap, then rerun the authorization check. The SLOW response is load-only under the owner rule.

After `git merge dev`, the local adversarial harness reported a non-load test failure before `share-options`: `authz_matrix.py` raised `OpenAPI operation has no authorization policy: GET /api/v1/analytics (analytics_report)` at its policy lookup. `attack.py` also logged one `SLOW` Note create (7.0 s). I am checking the merged Analytics route policy and will close the matrix gap, then rerun the authorization check. The SLOW response is load-only under the owner rule.
Author
Owner

#105 complete

Head: ad5d78ae35c6d0ebd495db6d9455fbdb878b435d (job/share-edit). git merge dev reported Already up to date. No push, deploy, or merge was done.

Built

Public links can grant Edit to one supported text item. Visitors enter a printable name. Text files use bounded UTF-8 read and compare-and-swap writes; Notes use the live collaboration room. Writes keep normal Versions and show the guest name in Version history. The guest token is bound to the link, item ID, password hash, verified client IP, and eight-hour expiry. The WebSocket checks the grant on frames and on its recheck interval. The Share dialog disables Edit when the item cannot be edited. Linked Notes are scoped to the shared item and remove private file references.

The adversarial share-options probe now covers binary targets, password rotation, revocation, expiry, REST access, and open WebSocket invalidation. The authorization matrix now classifies Analytics as per-User data after the merged dev route was missing from its policy table.

Files

  • Web: apps/web/src/lib/files/PublicLinkPage.svelte, apps/web/src/lib/files/ShareDialog.svelte, apps/web/src/lib/notes/NoteEditorSurface.svelte, apps/web/src/lib/notes/collab.ts, apps/web/src/lib/notes/editorHost.ts, apps/web/e2e/share.mjs
  • Rust: crates/calternal-collab/Cargo.toml, crates/calternal-collab/src/session.rs, crates/calternal-fs/src/root.rs, crates/calternal-fs/src/write.rs, crates/calternal-notes-core/src/lib.rs, crates/calternal-notes-core/src/links.rs, crates/calternal-server/src/wire.rs, crates/plugins/files/Cargo.toml, crates/plugins/files/migrations/0012_public_edit_history.sql, crates/plugins/files/migrations/0013_public_edit_sessions.sql, crates/plugins/files/src/lib.rs, crates/plugins/files/src/listing.rs, crates/plugins/files/src/public.rs, crates/plugins/files/src/thumbnails.rs, crates/plugins/notes/src/lib.rs, crates/plugins/notes/src/store.rs
  • API, docs, dependencies, tests: contracts/openapi.json, packages/api-client/src/generated.ts, packages/editor/src/components/ImageView.svelte, docs/DESIGN.md, Cargo.lock, tests/adversarial/attack2.py, tests/adversarial/authz_matrix.py

Continuation commits: 4783a4ee (test(shares): cover public Edit grant invalidation) and ad5d78ae (test(security): classify analytics report policy).

Verification

  • cargo fmt --check: exit 0; stdout was empty.
  • cargo clippy --all-targets -- -D warnings (exit 0):
    sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead
    sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead
    sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead
       Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/share-edit/crates/calternal-server)
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 22.87s
    
  • Full cargo test: 69 passing result summaries; 1,233 passed, 0 failed, 12 ignored. No failed-result markers. The command ended with:
       Doc-tests calternal_search
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    
       Doc-tests calternal_sync
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    
       Doc-tests calternal_tags
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    
  • bun run check:
    $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
    Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/share-edit/apps/web
    Getting Svelte diagnostics...
    
    svelte-check found 0 errors and 0 warnings
    
  • bun run test:
     Test Files  68 passed (68)
          Tests  534 passed (534)
       Duration  68.71s (transform 69%, environment 13%, import 11%, tests 6%, setup 1%)
    
    The run also printed Could not parse CSS stylesheet and repeated Not implemented: Window's scrollTo() method notices; all tests passed.
  • Production bun e2e/share.mjs passed all share flows, including live guest Note editing, the named caret, and Version attribution; the post-CLS-fix log ended with SHARE E2E PASSED.
  • AUTHZ_MATRIX_ONLY=1 tests/adversarial/run.sh: 229 OpenAPI operations × 4 identities = 916 requests; no !! findings. Output: Authorization matrix: 229 OpenAPI operations x 4 identities = 916 requests; policy classes {'public': 18, 'public_link': 14, 'user': 176, 'admin': 21}.
  • Initial broad adversarial run produced 17 SLOW latency findings while other worktrees compiled. The share probe's first launch also lacked the local media runner in PATH, so thumbnail checks returned 404 after the probe had marked setup SLOW. I reran share-options with the available media runner: ==== ROUND 2 FINDINGS 0. The HEIF/AVIF media probe passed. These run details are recorded here; no share API defect remained.

Decisions

The issue left guest name and resource limits open. This branch uses printable names of 1–80 characters, eight-hour guest sessions, 60 Edit requests per verified IP and 600 per link per minute, 300 WebSocket frames per IP/link per minute, a 2 MiB UTF-8 text limit, and a 3 MiB WebSocket frame limit for Yjs overhead. These choices were already recorded in the issue progress comments. I made no new product-level decisions in this continuation.

Known gaps

The shared host reported SLOW-only latency during the broad round. The default local adversarial launcher did not include the media runner in PATH; the isolated share probe passed when run with it. Web tests print the CSS/scrollTo() notices listed above while still passing.

# #105 complete Head: `ad5d78ae35c6d0ebd495db6d9455fbdb878b435d` (`job/share-edit`). `git merge dev` reported `Already up to date.` No push, deploy, or merge was done. ## Built Public links can grant Edit to one supported text item. Visitors enter a printable name. Text files use bounded UTF-8 read and compare-and-swap writes; Notes use the live collaboration room. Writes keep normal Versions and show the guest name in Version history. The guest token is bound to the link, item ID, password hash, verified client IP, and eight-hour expiry. The WebSocket checks the grant on frames and on its recheck interval. The Share dialog disables Edit when the item cannot be edited. Linked Notes are scoped to the shared item and remove private file references. The adversarial `share-options` probe now covers binary targets, password rotation, revocation, expiry, REST access, and open WebSocket invalidation. The authorization matrix now classifies Analytics as per-User data after the merged `dev` route was missing from its policy table. ## Files - Web: `apps/web/src/lib/files/PublicLinkPage.svelte`, `apps/web/src/lib/files/ShareDialog.svelte`, `apps/web/src/lib/notes/NoteEditorSurface.svelte`, `apps/web/src/lib/notes/collab.ts`, `apps/web/src/lib/notes/editorHost.ts`, `apps/web/e2e/share.mjs` - Rust: `crates/calternal-collab/Cargo.toml`, `crates/calternal-collab/src/session.rs`, `crates/calternal-fs/src/root.rs`, `crates/calternal-fs/src/write.rs`, `crates/calternal-notes-core/src/lib.rs`, `crates/calternal-notes-core/src/links.rs`, `crates/calternal-server/src/wire.rs`, `crates/plugins/files/Cargo.toml`, `crates/plugins/files/migrations/0012_public_edit_history.sql`, `crates/plugins/files/migrations/0013_public_edit_sessions.sql`, `crates/plugins/files/src/lib.rs`, `crates/plugins/files/src/listing.rs`, `crates/plugins/files/src/public.rs`, `crates/plugins/files/src/thumbnails.rs`, `crates/plugins/notes/src/lib.rs`, `crates/plugins/notes/src/store.rs` - API, docs, dependencies, tests: `contracts/openapi.json`, `packages/api-client/src/generated.ts`, `packages/editor/src/components/ImageView.svelte`, `docs/DESIGN.md`, `Cargo.lock`, `tests/adversarial/attack2.py`, `tests/adversarial/authz_matrix.py` Continuation commits: `4783a4ee` (`test(shares): cover public Edit grant invalidation`) and `ad5d78ae` (`test(security): classify analytics report policy`). ## Verification - `cargo fmt --check`: exit 0; stdout was empty. - `cargo clippy --all-targets -- -D warnings` (exit 0): ```text sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead sccache: warning: The server looks like it shut down unexpectedly, compiling locally instead Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/share-edit/crates/calternal-server) Finished `dev` profile [unoptimized + debuginfo] target(s) in 22.87s ``` - Full `cargo test`: 69 passing result summaries; 1,233 passed, 0 failed, 12 ignored. No failed-result markers. The command ended with: ```text Doc-tests calternal_search running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests calternal_sync running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests calternal_tags running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` - `bun run check`: ```text $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/share-edit/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` - `bun run test`: ```text Test Files 68 passed (68) Tests 534 passed (534) Duration 68.71s (transform 69%, environment 13%, import 11%, tests 6%, setup 1%) ``` The run also printed `Could not parse CSS stylesheet` and repeated `Not implemented: Window's scrollTo() method` notices; all tests passed. - Production `bun e2e/share.mjs` passed all share flows, including live guest Note editing, the named caret, and Version attribution; the post-CLS-fix log ended with `SHARE E2E PASSED`. - `AUTHZ_MATRIX_ONLY=1 tests/adversarial/run.sh`: 229 OpenAPI operations × 4 identities = 916 requests; no `!!` findings. Output: `Authorization matrix: 229 OpenAPI operations x 4 identities = 916 requests; policy classes {'public': 18, 'public_link': 14, 'user': 176, 'admin': 21}`. - Initial broad adversarial run produced 17 SLOW latency findings while other worktrees compiled. The share probe's first launch also lacked the local media runner in `PATH`, so thumbnail checks returned 404 after the probe had marked setup SLOW. I reran `share-options` with the available media runner: `==== ROUND 2 FINDINGS 0`. The HEIF/AVIF media probe passed. These run details are recorded here; no share API defect remained. ## Decisions The issue left guest name and resource limits open. This branch uses printable names of 1–80 characters, eight-hour guest sessions, 60 Edit requests per verified IP and 600 per link per minute, 300 WebSocket frames per IP/link per minute, a 2 MiB UTF-8 text limit, and a 3 MiB WebSocket frame limit for Yjs overhead. These choices were already recorded in the issue progress comments. I made no new product-level decisions in this continuation. ## Known gaps The shared host reported SLOW-only latency during the broad round. The default local adversarial launcher did not include the media runner in `PATH`; the isolated share probe passed when run with it. Web tests print the CSS/`scrollTo()` notices listed above while still passing.
Author
Owner

Merged into dev at 371e53a0 (share e2e incl. live guest editing passes; authorization matrix 916 requests clean). Deploy status on #203.

Merged into dev at 371e53a0 (share e2e incl. live guest editing passes; authorization matrix 916 requests clean). Deploy status on #203.
kayg closed this issue 2026-09-27 05:37:26 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#105
No description provided.