Skip to content

Core sync: shed route-level policy special cases as core invariants land (core #59, #57, #65, #67) #41

Description

@cuibonobo

The server currently enforces three pieces of policy in route handlers — added by #9 as stopgaps. The design review moved each of them into core's invariant layer, where they're written once and inherited by every adapter and server. Tracking issue for removing the duplicates as core ships, so the server converges on "thin wire shell over ScopedStack."

Route guard today Location Core home
_attachment@1 immutable fields (fileId, size, mimeType) on PATCH src/routes/records.ts:228-239 Record validation per haverstack/core#65 (mimeType conflict rule) — coordinate what remains for fileId/size with the owner; if core doesn't take those, keep the guard and say so in a comment
_grant@1 PATCH/DELETE owner-only records.ts:240-246, 260-266 haverstack/core#57 (grantee out of entityId, listGrants/revoke scope) + core #59's owner/creator-only pattern for privilege-bearing verbs
?hard=true owner-only on DELETE records.ts:257 haverstack/core#59's one-line carve-out at ScopedStack.delete (with the open question: non-owner {hard:true} → error vs silent soften, leaning error → 403 round-trip)

Why removal matters (not just hygiene)

The interim guards have real defects that inherit-from-core eliminates:

  1. They consult the unscoped stack before the permission check. Both PATCH and DELETE call stack.get(id) (no requester scoping, records.ts:226, 259) and can answer 422/403 based on the record's type before ScopedStack ever evaluates visibility. A requester who can't read a record can still learn it exists and is a grant/attachment — a small oracle the spec's deliberate 403-vs-404 discipline doesn't intend to hand out.
  2. Check-then-act: the guard reads, then the scoped mutation re-reads — a TOCTOU window that core-level enforcement (inside the same code path as the write, atop core Attachment upload buffers the entire body in memory before the size check #46's transactions) closes.
  3. Route guards protect exactly two routes; core guards protect every path (restore, future endpoints, other servers).

Also in scope once core lands: _config protection (delete → 409, entityId change → 409) arrives entirely via haverstack/core#67 — server work is only the #32 fixtures; and any wire surface for listGrants/revoke decided under core #57's follow-up gets its endpoints here.

Work items

Refs haverstack/core#59, haverstack/core#57, haverstack/core#65, haverstack/core#67, haverstack/core#51, #32, #33.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions