Skip to content

feat(api): strict api key validation with issue paths - #227

Merged
josue-commits merged 5 commits into
developfrom
feat/api-v1-strict-validation
Oct 13, 2026
Merged

josue-commits merged 5 commits into
developfrom
feat/api-v1-strict-validation

Conversation

@josue-commits

Copy link
Copy Markdown
Contributor

Summary

API v1 validation errors now list every issue with its dot path, and writes made with an API key are held to what the form editor can show. A form created or edited by API stays fully editable in the builder.

Changes

  • issues[] with dot paths (for example config.steps.2.question) on admin CRUD, branding and destinations.
  • Strict config rules apply only to API key callers; session callers keep the legacy behavior (explicit test).
  • Editor defaults (buttonText, reveal.enabled, scheduler.prefill) are accepted on any step type, matching the builder's type change.
  • Webhook destination rules run where destinations are stored (POST /v1/forms and PUT .../destinations), not on PUT /v1/forms/:id.
  • STEPS_REQUIRED writes nothing; the normalized config is returned.
  • formGuid and formSignature are stripped on POST for every caller.
  • Brand kit logos are compared by value (isDeepStrictEqual), not key order.

How it was tested

  • pnpm typecheck green (Node 22).
  • pnpm test green: api 768, web 1248, db 353, engine 436, destinations 125, types 59, shared 83, notifications 57.
  • Publish gate passed; pnpm changeset status OK.
  • Postgres not run: no SQL or migrations changed.

Review notes

Fixed by the independent review:

  • Webhook signatureHeader/timeoutMs refused on PUT forms/:id even though destinations are not saved there (7e2c000).
  • Builder defaults refused on a retyped step (f81411d, regression test).
  • Logos compared with key-order-sensitive JSON (7e2c000, regression test).

Known edges, unchanged by design: stored logos refused once the kit changes; blank-title message/scheduler steps refused (EMPTY_QUESTION); paths carry the config. prefix.

🤖 Generated with Claude Code

josue-commits and others added 5 commits October 12, 2026 17:49
A 400 from the form, branding and destinations routes now carries
`issues: [{ path, message, code }]` with every zod issue, `path` being the
dot path inside the request body (`config.steps.2.question`). `message`
stays the first issue's, so a client that reads only `message` sees no
change. One shared `parse` replaces the three copies that kept only the
first issue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
…/forms

A config copied from another form carries that form's `formGuid` and
`formSignature`, and keeping them aims the new form at the original's
mirror form in the portal. Creating now strips them for every caller,
with the same helper duplicate uses, now exported from @quill/db.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
With `via: 'api_key'` only, a form config is refused with
UNSUPPORTED_FIELD and the exact path when it carries a field the editor
has no control for (the spec's research list), a step without a question
is refused with EMPTY_QUESTION, and a PUT config without `steps` answers
400 STEPS_REQUIRED before anything is written. The webhook fields are
also refused on PUT /v1/forms/:id/destinations. The issues join the
schema's in one 400.

The value the editor itself writes passes (the derived flowGroup,
"Continue" on a message, reveal.enabled and scheduler.prefill true, the
brand kit's own client logos): every builder step carries those, and so
does the normalized config this API answers with, so refusing them would
refuse a form read and sent straight back.

A key's config is saved as `normalizeConfig` returns it, and the
response carries it. A session saves exactly as before, legacy fields
included, with an explicit regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
Changing a step's type in the builder keeps what createEmptyStep seeded
(buttonText "Continue", reveal.enabled true, scheduler.prefill true) and
only resets flowGroup. A builder form with a retyped step was refused with
UNSUPPORTED_FIELD when an API key sent it straight back. Those seeded values
now pass on any step type; flowGroup stays tied to the step's type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
PUT /v1/forms/:id drops destinations from the draft, so refusing webhook
signatureHeader/timeoutMs there refused every edit of a form whose live
webhook already carried one, and removing it from the body never helped.
The webhook rule now runs where destinations are written: POST /v1/forms
and PUT /v1/forms/:id/destinations.

The brand kit client logos comparison used JSON.stringify, so the same
logos with their keys in another order were refused. It now uses
isDeepStrictEqual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
@josue-commits
josue-commits merged commit 705d222 into develop Oct 13, 2026
6 checks passed
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.

1 participant