Skip to content

feat(core): wire parsers for the remaining spec'd endpoints, and strict element keys - #356

Merged
cuibonobo merged 4 commits into
mainfrom
claude/issue-354-k0t15f
Sep 26, 2026
Merged

cuibonobo merged 4 commits into
mainfrom
claude/issue-354-k0t15f

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Closes #354.

Wire parsers. Every endpoint core specifies now has a parser in @haverstack/core/wire. A server no longer needs its own copies of core's param and field names (as in haverstack/server#130's knownParams() / readJson() lists).

Endpoint Parser
POST /auth/challenge parseAuthChallengeBody()
POST /auth/token parseAuthTokenBody()
PATCH /entity parseEntityPatchBody()
POST /types parseTypeBody()
POST /records/:id/migrate parseMigrationBody()
DELETE /records/:id parseDeleteParams()
GET /attachments/:fileId parseDownloadParams()
GET /records/:id/associations parseAssociationParams()

Each parser answers bad input the way POST /records already does:

  • 400: an unknown name, a malformed boolean, a repeated single-value param, or a missing required field.
  • 422, with the field's path: a known field whose value has the wrong type.

The body parsers live in a new wire-body.ts, which also holds the helpers wire-record.ts now imports from it.

Element keys. Stack now refuses a key that an element's kind does not define, with a 400. This covers association, permission, relationship-target and grant-target elements, at every depth. It applies to:

  • create(), mutate(), associate(), dissociate(), grantAccess(), revokeAccess()
  • relatedTo filter targets
  • grantType(), revokeType(), listTypeGrants()

A _grant record's grantee is held to the same keys and refused as content with a 422. The key tables are typed against each union arm, so adding a field to an arm fails to compile until the field is listed.

Spec

  • docs/spec/wire-format.md § Unrecognized input: a table matching each endpoint to its parser, the 400/422 split, and element keys added to "at every depth".
  • docs/spec/wire-format.md § Types: baseId, version, schemaHash and createdAt are accepted and ignored.
  • docs/spec/data-model.md § Associations: an element carries only the keys its kind defines.
  • New error fixtures: error-bad-request-non-boolean-purge, error-bad-request-unknown-auth-token-key and error-bad-request-unknown-grantee-key.

Verification

All six checks pass locally: pnpm run format:check, check:refs, lint, test, build and typecheck.

  • New tests: packages/core/tests/wire-body.test.ts and element-keys.test.ts, plus additions to wire-request.test.ts.
  • One test turns a whole StackType into JSON the way APIAdapter.saveType() sends it and runs it through parseTypeBody().
  • Another checks that a malformed schema still gets to defineType() and comes back as a 422.

Notes for reviewers

  • schemaHash: POST /types accepts and ignores schemaHash, which the issue didn't list. APIAdapter.saveType() sends the whole Type, so refusing it would break every client.
  • Why refusing element keys on write strands nothing: the SQLite adapters already dropped these keys, because they only store keys they have a column for. MemoryAdapter kept them, so the two adapters disagreed about the same write, and this removes that disagreement. There's also no install base. The one cost: a foreign export whose elements carry extension fields is now refused on import, where it used to lose those fields silently.
  • Auth endpoints: a malformed body on the handshake endpoints now gets bad_request or validation. invalid_did still covers a string that isn't a verifiable DID.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ah52eTkayJ1JxPuEmgVGe


Generated by Claude Code

Every endpoint core specifies now has a ./wire parser, so a server no
longer copies core's param and field names into its own lists, where they
drift: parseAuthChallengeBody, parseAuthTokenBody, parseEntityPatchBody,
parseTypeBody, parseMigrationBody, parseDeleteParams, parseDownloadParams
and parseAssociationParams. Each refuses an unknown name or malformed
boolean with 400 and a wrong-typed known body field with 422, following
the split POST /records already makes.

The body helpers are shared with wire-record.ts. The spec's
§ Unrecognized input now tables the parser for each endpoint, § Types
names the keys POST /types accepts and ignores, and two error fixtures
pin a non-boolean purge and an unknown /auth/token key.

Unknown keys inside grant and association elements are left for a
separate decision, as the issue notes.

Refs #354

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ah52eTkayJ1JxPuEmgVGe
…target elements

validateAssociation() and validateGrantTarget() checked that required
fields were present but not that others were absent, so
{ kind: 'entity', entityId, scope } on a grantee stored and answered 200.
The SQLite adapters then dropped the extra key because they have no
column for it, while MemoryAdapter kept it, so the two disagreed about
the same write.

Every element is now held to its own kind's keys, at every depth, with
400. The key tables are typed against each union arm, so a new field
fails to compile until it is listed. _grant content applies the same
keys to its grantee as a 422. wire-request.ts reuses the shared
target-key table.

Refusing on write strands nothing: SQLite never stored such keys, and
there is no install base.

Refs #354

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014ah52eTkayJ1JxPuEmgVGe
@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 73e98de

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@haverstack/core Minor
@haverstack/conformance-fixtures Minor
@haverstack/adapter-api Patch
@haverstack/adapter-conformance Patch
@haverstack/adapter-local Patch
@haverstack/blob-adapter-disk Patch
@haverstack/blob-adapter-s3 Patch
@haverstack/commons Patch
@haverstack/record-adapter-do-sqlite Patch
@haverstack/record-adapter-sqlite Patch
@haverstack/wire-types Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

parseEntityPatchBody() reads { content }, but § Entity named no body,
so a client copying PATCH /records/:id's contentPatch got a 400 the
spec did not explain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01STGzVnzxKKwGh3bs6RU5Fe
content on the wire means whole content (POST /records, migrate), so a
merging key spelled content was the trap data-model.md § Mutations
names. The parser is new in this PR, so its existing changeset covers it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01STGzVnzxKKwGh3bs6RU5Fe
@cuibonobo
cuibonobo merged commit 3e23797 into main Sep 26, 2026
9 checks passed
@cuibonobo
cuibonobo deleted the claude/issue-354-k0t15f branch September 26, 2026 12:20
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.

Wire parsers for the spec'd endpoints core doesn't parse yet

2 participants