Skip to content

Upgrade to core 0.13 line and adopt the 404/403 anti-oracle rule - #87

Merged
cuibonobo merged 1 commit into
mainfrom
claude/issue-79-plan-7ngsh3
Aug 28, 2026
Merged

Upgrade to core 0.13 line and adopt the 404/403 anti-oracle rule#87
cuibonobo merged 1 commit into
mainfrom
claude/issue-79-plan-7ngsh3

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Closes #79.

Summary

  • Bumps @haverstack/core to ^0.13.1, wire-types to ^0.12.0, adapter-local to ^0.12.0, commons to ^0.5.0, and conformance-fixtures to ^0.6.0 (dev). Confirmed core resolves to 0.13.1 and record-adapter-sqlite to 0.5.1 transitively.
  • Adopts the anti-oracle rule: ScopedStack.get() now returns null (rather than throwing) for a record the requester can't read, and the write/history mutators throw StackNotFoundError instead of StackPermissionError in that case — 403 is now earned by readability, not by holding a valid id.
  • An anonymous requester now gets 404 + WWW-Authenticate: Bearer for a private record instead of 403 or a bodyless 401 — added centrally in errorMiddleware (fires whenever a StackNotFoundError-mapped 404 reaches an anonymous requester), so it applies uniformly rather than per-route. Bearer (RFC 6750 §3) is the standard WWW-Authenticate scheme for a bearer-token API — this server's actual wire credential — rather than the higher-level did-challenge exchange discovery advertises for obtaining that token.
  • Fixed entity.ts's cached owner-record id: it's now only evicted by the owner's own null read. A non-owner's null is ambiguous under the new rule (denied vs. gone) and previously forced a wasted re-resolve query on every forbidden GET /entity. Added a regression test asserting the query isn't re-run.
  • requireOwner()-gated routes (tokens, types, entity PATCH, attachment delete/gc) are unaffected — those throw in middleware before any record is read, so they correctly stay 403.
  • retainSortDirection's existing validation in queryParsing.ts is unchanged and still the right defense-in-depth alongside core's own hardened ORDER BY check.

Also folded in (shipped in the same dependency range, needed to get the suite green)

Two wire-contract changes were bundled into the same core/wire-types minors and aren't mentioned in #79's checklist, but the suite can't pass without them:

  • Mutation responses: soft-delete, associate, dissociate, and setPermissions now answer 200 with the updated record instead of 204 (hard-delete, which produces no record, is unchanged and stays 204).
  • Actor stamping: a non-author PATCH now stamps updatedBy/updatedVia on the record. This "arrives for free" — ScopedStack already does it from the session, no route change needed, just a conformance test dispatching the new fixture.

Sweep of toBe(403) assertions (#79's checklist item)

Went through all 29 toBe(403) assertions across tests/. Beyond the two named in the issue (records.test.ts:110,142), found three more read-based cases that also needed to flip to 404, confirmed by actually running the suite against the bumped deps rather than by inspection alone:

  • records.test.ts — "an empty permissions array makes the record private" (anonymous GET after PUT /permissions)
  • conformance.test.tsset-permissions-empty-is-private fixture dispatch (same shape)
  • entity.test.tsGET /entity for a non-owner authenticated entity (only requireAuth()-gated, not requireOwner(), so it goes through the same get()-returns-null path)

One test (records.test.ts — soft-DELETE on a _grant record) also flipped, but for a different reason: stack.grant() never gave the grantee read access to the grant record itself, so the test was accidentally exercising the anti-oracle case instead of the write-protection fence it was meant to test. Fixed by granting explicit read+write on the record directly, matching its PATCH sibling test.

Test plan

  • pnpm test — 343/343 passing
  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm format:check — clean
  • New change-feed discovery fixtures (discovery-advertises-a-change-feed, discovery-advertises-a-feed-that-neither-resumes-nor-includes-records) parked in the discovery block's SKIPPED set with a reason pointing at Implement GET /changes (SSE change feed) #82/Advertise the change feed in discovery #83
  • No anti-oracle logic added to routes — ScopedStack owns it, per the issue's acceptance criteria

Generated by Claude Code

Bumps @haverstack/core to ^0.13.1, wire-types to ^0.12.0, adapter-local
to ^0.12.0, commons to ^0.5.0, and conformance-fixtures to ^0.6.0 (dev),
resolving core to 0.13.1 and record-adapter-sqlite to 0.5.1 transitively.

ScopedStack.get() now returns null (rather than throwing) for a record
the requester can't read, and the write/history mutators throw
StackNotFoundError instead of StackPermissionError in that case, so
403 is now earned by readability rather than by holding a valid id.
Record ids encode their creation millisecond, so a bare 403 to an
unauthenticated caller would let anyone confirm a guessed id's
same-millisecond siblings — errorMiddleware now answers an anonymous
404 with a Bearer WWW-Authenticate challenge, keeping the login prompt
reachable without reopening that distinction.

entity.ts's cached owner-record id is now only evicted by the owner's
own null read, since a non-owner's null is ambiguous under the new
rule (denied vs. gone) and previously forced a wasted re-resolve query
on every forbidden GET.

Also folds in two wire-contract changes that shipped in the same
dependency range and are needed to get the suite green: soft-delete,
associate, dissociate, and setPermissions now answer 200 with the
updated record instead of 204 (hard-delete, which produces no record,
is unchanged); and a non-author PATCH now stamps updatedBy/updatedVia
on the record, which ScopedStack already does from the session with
no route change needed.

The two new change-feed discovery fixtures are parked in conformance
tests' SKIPPED set pending #82/#83.
@cuibonobo
cuibonobo merged commit 9e08dbc into main Aug 28, 2026
4 checks passed
@cuibonobo
cuibonobo deleted the claude/issue-79-plan-7ngsh3 branch August 28, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade to the core 0.13 line and adopt the 404/403 anti-oracle rule

2 participants