mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-04 04:21:38 +00:00
f1c9457790
* feat(web): 403-driven cache purge for localIndex (TASK-1360) Per DOC-1342 design decision #3: the local cache is "what you could see last time you synced." When the server returns 403 mid-session, the offending entry is purged so the next read doesn't surface stale-by-permission rows. ## API client (web/src/lib/api/client.ts) - New `AccessRevokedScope` type + `setAccessRevokedHandler` registration hook. Keeps client.ts free of any store import (no circular dep). - `request()` on 403 parses the URL with `parseAccessRevokedScope` (handles item endpoints `/workspaces/{ws}/items/{idOrSlug}` and collection-items endpoints `/workspaces/{ws}/collections/{coll}/items`) and invokes the handler. Handler failures are caught + logged; the 403 still propagates as a `PadApiError`. ## localIndex - New `removeByCollection(ws, collSlug)` — bulk-remove every row in a collection. Used when a collection-scoped 403 means the whole grant is revoked. - New `findByIdOrSlug(ws, idOrSlug)` — id-first then slug-scan lookup so an item-scoped 403 with a slug URL can resolve to the in-RAM id (the SvelteMap is keyed by id). ## App bootstrap (+layout.svelte) - Registers the handler once at top level: item → `remove` after `findByIdOrSlug` resolves; collection → `removeByCollection`. Both purges write through to IDB via the existing `persistRemovals` path so reloads don't resurrect the stale row. 401 already triggers a /login redirect; this lands as the 403 counterpart. Permission-revocation that doesn't trigger a 403 (e.g. visibility loss with no fetched row) is explicitly punted per DOC-1342 #3 — best-effort cache, not authoritative for permissions. Parent: PLAN-1343. See DOC-1342 design decision #3. * fix(web): only purge on GET 403s, not write-method 403s (Codex round 1) [P1] notifyAccessRevoked fired on every 403 — including POST /items (create), PATCH /items/{id} (update), DELETE /items/{id} (archive), and the grants/share-link write endpoints. A read-only user who correctly fails to create or modify an item would have their entire collection purged from localIndex. Gate the purge on read methods (GET / HEAD). Write-method 403s mean "you can read but not write" — the cached rows are still legitimately visible. Read-method 403s are the canonical "visibility revoked" signal. Parent: PLAN-1343. * fix(web): purge entire workspace on 403, not per-item/collection (round 2) [P1] Codex caught two related issues: 1. Pad's server returns 403 from the workspace-access middleware (`permission_denied`, `not a member of this workspace`), and item-level visibility misses return 404. A 403 on /workspaces/{ws}/items/{slug} therefore means workspace access is gone — purging only `slug` leaves the rest of the workspace's cache stale. 2. The item URL parser matched `/items/{slug}/grants`, `/items/{slug}/share-links`, etc. — owner-only subroutes that can 403 after a role downgrade while the item itself is still readable. Purging the item on those was incorrect. Both fixed by collapsing the scope to "the entire workspace": AccessRevokedScope is now `{ kind: 'workspace'; workspace }`; the parser just extracts the workspace slug from any `/workspaces/{ws}/...` path; the handler in +layout.svelte calls `localIndex.reset(ws)`. The per-item `findByIdOrSlug` and `removeByCollection` helpers added in round 0 stay in localIndex for future per-item revocation paths (e.g. server-side `unauthorized` SSE events), but the 403 path no longer uses them. Parent: PLAN-1343. * fix(web): scope 403 purge to read-model endpoints only (Codex round 3) [P1] Codex caught that workspace-scoped 403s aren't all workspace-access-revoked signals. Grant-only guests legitimately get 403 on /workspaces/{ws}/members, /workspaces/{ws}/storage/usage, etc., while their item read access is fine. The previous "any workspace-scoped 403 → reset" handler would wipe the local index on every such 403, leaving guest views stuck loading. Restrict parseAccessRevokedScope to the explicit read-model endpoints the local-first store actually consumes: GET /workspaces/{ws}/items GET /workspaces/{ws}/items/{idOrSlug} GET /workspaces/{ws}/items-index GET /workspaces/{ws}/items-changes GET /workspaces/{ws}/collections/{coll}/items A 403 on any of these means the cache is stale-by-permission. A 403 on anything else stays opaque to the local index. Parent: PLAN-1343.