Repository navigation
feat: adopt core 0.43's per-aspect writes, wire parsers and installs - #131
Merged
Merged
Conversation
Bring the server up to @haverstack/core 0.43 (adapter-local 0.42,
commons 0.38, wire-types 0.42, conformance-fixtures 0.37).
One rule per concern rather than per-route exceptions:
- Every aspect of a record has one write endpoint: PATCH .../content,
PUT .../parent, PUT .../unlisted, and POST (edit) / PUT (replace) on
.../associations and .../permissions. The change-set PATCH and the
/delete sub-paths are gone; PATCH /entity is PATCH /entity/content.
- Every write reads and checks If-Match, including the no-bump ones.
- Every endpoint core specifies is parsed by core's parser for it;
knownParams()/readJson keys remain only for server-defined routes.
The server's own booleanParam, isOwnerActingAlone and the POST /types
and migrate hand checks are removed.
- Versions and the journal page by one rule (readHistoryPage): a
500-row server ceiling announced through an exact cursor. The query
limit clamp is gone, since Stack resolves and caps query defaults.
- POST /records/:id/migrate is ScopedStack's to gate, so an installed
app can commit migrations within its own families.
- GET /records/:id hides tombstones unless ?includeDeleted=true.
New: POST /installs (advertised in discovery) and an owner-only review
surface at /installs/requests. Pending requests live in a new sibling
store, <DB_PATH>.installs — additive, no migration of existing data.
Also: LocalAdapter.open({ create }), includeUnlisted read from the
change filter, the full commons set seeded (image, post, site), and
the conformance gate dispatching every 0.37 fixture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
Review findings on the install surface: - GET /installs/requests/:id returns an ETag naming the plan it shows, a hash of its canonical JSON. Approve reads If-Match and answers 412 (precondition_failed) when the plan has changed, so what lands is what was reviewed — the same If-Match rule every other write follows. - Requests are removed by the id that names them, never by key, so a manifest re-sent mid-approval is no longer deleted in its place. - requireActingAlone() refuses a delegated session before its body is read, in the same order as requireAuth() and requireOwner(). - Manifests compare as canonical JSON, so reordered keys keep an id. - ifMatchVersion()/ifMatchTag() live in src/lib/ifMatch.ts, and serializePlan spreads every plan field rather than listing them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
Fencing an approval to the reviewed plan is core's invariant: installApp() already re-plans and compares planFingerprint(), and refuses a stale plan with StackConflictError. A server-side plan hash carried in If-Match gave the header a second meaning beside a record version, answered the same stale plan with 412 or 409 depending on timing, and minted an error code outside WireErrorCode. Approval re-plans unconditionally until core lets installApp() take the fingerprint the owner reviewed. Also pins that GET /records/:id/associations and /permissions answer 404 for a soft-deleted record, as GET /records/:id does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
…ield
- Approve answers WireInstallResponse ({ status: 'installed', install }),
the body POST /installs gives the key for the same outcome.
- serializePlan names each field, and its return type covers every
InstallPlan field, so a field core adds neither reaches the wire
unchosen nor drops out of the owner's review unnoticed: it fails to
compile until someone decides. The lint exception it needed goes.
- The handshake token and a pending install request share one
self-service lifetime, SELF_SERVICE_TTL_MS, rather than two literals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the server up to the latest published core releases:
@haverstack/core0.43,adapter-local0.42,commons0.38,wire-types0.42 andconformance-fixtures0.37. Where the old code had a per-route exception, this applies one shared rule:PATCH /records/:id/content,PUT .../parent,PUT .../unlisted, plusPOST(edit list) andPUT(replace) on.../associationsand.../permissions. The change-setPATCH /records/:idand the/deletesub-paths are removed.PATCH /entityis nowPATCH /entity/content.If-Matchthrough one helper (src/lib/ifMatch.ts), including writes that don't bump the version.booleanParam,isOwnerActingAlone, and the hand-written checks onPOST /typesand migrate.knownParams()and thereadJsonkey list remain only for routes the server defines itself.{ versions, cursor }envelope) and the journal sharereadHistoryPage: a 500-row page cap, with a cursor that is null exactly when nothing follows. The querylimitclamp is removed, becauseStacknow sets and caps query defaults itself.ScopedStack, so an installed app can commit migrations within its own type families.GET /records/:idhides a tombstone unless called with?includeDeleted=true.GET .../associationsand.../permissionsare views of the live record, so they answer404for a soft-deleted one and take noincludeDeleted. A tombstone carries no associations, and its permissions are onGET /records/:id?includeDeleted=true.POST /installsis advertised in discovery./installs/requestsis an owner-only surface to list, review the current plan, approve and decline. Approve answers the same{ status: 'installed', install }body thatPOST /installsdoes.idthat names it.requireActingAlone()refuses a delegated session before its body is read.SELF_SERVICE_TTL_MS).LocalAdapter.open({ create }),includeUnlistedread from the change filter, all ten commons types seeded (adds image, post and site), andgrantTypecalls take a type family.Stored data: pending install requests live in a new sibling file,
<DB_PATH>.installs. It is additive, and nothing existing needs migrating.Docs
docs/api.md: Discovery, Records (per-aspect writes, history/restore/migrate, optimistic concurrency), query defaults, tombstones, Unlisted, change-feedkinds, Types, the new Installs section, Entity, Permissions and the grant surface (baseId).docs/deployment.md: the.noncesand.installssibling stores, and how install requests are bounded..env.example: the full commons type list.The wire contract itself is core's. This PR implements core 0.43 and does not change the contract.
Verification
pnpm run format:check && pnpm run lint && pnpm typecheck && pnpm testall pass, with 660 tests across 33 files.pnpm run buildalso succeeds.The conformance gate dispatches every 0.37 fixture. The only skips are the same two as before, each of which pins a client obligation rather than a server one.
Notes for reviewers
If-Matchon a write that doesn't bump the version now answering412.installApp()still refuses a plan the stack moved on from while it applies. Checking that the plan is still the one the owner reviewed belongs to core: feat(core): every installApp() names the plan it approves core#478 addsinstallApp(plan, { fingerprint }). The server will pick it up in the follow-up update that adopts the next core release.serializePlannames each field it shows, and its return type has to cover everyInstallPlanfield. A field core adds (such as #478'sfingerprint) therefore fails to compile here until someone decides whether the review shows it.delete-record-softfixture's body keeps content that the spec's tombstone projection empties. The test pins the spec. Filed as delete-record-soft fixture shows content the tombstone projection empties core#475.mainhas three unreleased changesets (theownerEntityIdrename, portable copy, reshare-needs-read). They're for the same follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8