Skip to content

feat(form-builder): phase-2 sweep onto core primitives - #144

Merged
olliethedev merged 2 commits into
v3from
feat/form-builder-phase2-sweep
Aug 18, 2026
Merged

feat(form-builder): phase-2 sweep onto core primitives#144
olliethedev merged 2 commits into
v3from
feat/form-builder-phase2-sweep

Conversation

@olliethedev

Copy link
Copy Markdown
Collaborator

Summary

Phase-2 adoption sweep for the form-builder plugin (part of #129), mirroring the CMS sweep in #143:

  • Resource factory: declares formBuilderResources (queries + mutations with invalidation/setData) and generates all hooks via createResource; form-builder-hooks.tsx is now a thin public wrapper layer with unchanged signatures. SSR loaders prefetch directly on the factory query entries.
  • Editor on resource useForm: save flow moved to useFormBuilderForm — server field errors render inline under the name/slug inputs, success/error toasts are localized, and create redirects to the edit page.
  • Search: bounded search param on the forms list API (max(200) in the schema, DEFAULT_MAX_PAGE_SIZE DB scan cap, filtered totals) plus a URL-synced search box on the list page via useListState (300ms debounce, replace history, re-seeds from back/forward navigation). SSG discriminator updated in lockstep.
  • notify + i18n: sonner replaced with useNotify(); every UI string now flows through useTranslate() with localization overrides taking precedence (new form-builder-renderer catalog for the public renderer).
  • Permissions: pages wrapped in ComposedRoute with permission props; CanAccess around New/Edit/Delete/Submissions controls.
  • Cleanup: local error helpers, SHARED_QUERY_CONFIG, and hand-wired query plumbing deleted; registry JSON regenerated.

Test plan

  • pnpm typecheck, pnpm lint, knip --strict all green
  • Full @btst/stack unit suite: 417 tests / 36 files passing, including new query-key parity guard, getters search coverage, and a jsdom client-sweep suite (CanAccess, useListState search sync, notify, i18n precedence)
  • Registry regenerated and JSON validated
  • Codegen E2E (CI): new "search filters the forms list and syncs the URL" spec plus existing form-builder smoke flows

Made with Cursor

Migrate the form-builder plugin to the v3 core primitives, mirroring the
CMS sweep:

- Declare formBuilderResources and generate all hooks via createResource;
  form-builder-hooks.tsx is now a thin public wrapper layer
- Move the editor save flow onto the resource useForm (server field
  errors inline, success/error toasts, create->edit redirect)
- Add a bounded search param to the forms list API (schema max(200),
  DEFAULT_MAX_PAGE_SIZE scan cap) and a URL-synced search box on the
  list page via useListState (debounce + external re-seed)
- Replace sonner with useNotify and route all UI strings through
  useTranslate with localization overrides (new renderer catalog)
- Wrap pages in ComposedRoute with permission props and CanAccess
  around New/Edit/Delete/Submissions controls
- Simplify SSR loaders to prefetch(Infinite)Query on factory entries
- Tests: query-key parity guard, getters search coverage, client-sweep
  jsdom suite; E2E search spec; registry regenerated

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview Aug 18, 2026 10:10pm
better-stack-playground Ready Ready Preview Aug 18, 2026 10:10pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review — No High-Confidence Vulnerabilities Found

I reviewed the full diff against the threat checklist. No high-confidence vulnerabilities were introduced. Details of every area examined are below.


1. Injection risks (SQL / command / template / path traversal)

Result: Not vulnerable.

The new search query parameter is validated at the API boundary by the existing Zod schema (z.string().max(200).optional()). Critically, the search term is never injected into a database query. The implementation fetches up to DEFAULT_MAX_PAGE_SIZE (1000) rows with only equality-filter whereConditions, then performs the free-text match in-memory using String.prototype.includes() on already-serialized data (form.name, form.slug). No adapter-level filter, no template interpolation, no command execution.

2. Auth/authz bypasses and permission boundary mistakes

Result: Not bypassed.

The onBeforeListForms hook (the caller-supplied authorization check) is invoked in plugin.ts before getAllForms is called with the search parameter. The search value does not affect the authorization decision; it only filters results after access is granted. The route handler sequence is:

1. Validate query params (Zod)
2. createContext(ctx.headers)   ← auth context
3. onBeforeListForms(context)   ← authorization gate (unchanged)
4. getAllForms(adapter, { ..., search })  ← guarded read

The new CanAccess wrappers in the list and submissions pages are UI affordance guards only (they hide action buttons). Server-side authorization is provided entirely by the backend hooks, which are unchanged.

The new ComposedRoute permission props on FormBuilderPageComponent and SubmissionsPageComponent add explicit client-side route-level access checks—a positive improvement.

3. Secrets handling, token leakage, insecure logging

Result: No issues.

No secrets, credentials, or sensitive tokens are introduced or logged. The localization refactor replaces static string constants with useTranslate(); nothing in the i18n strings constitutes sensitive data.

4. Unsafe deserialization, SSRF, XSS, request forgery

Result: No regressions introduced.

  • Submission data is displayed via JSON.stringify(viewSubmission.parsedData, null, 2) inside a <pre> element rendered by React—React escapes text node content by default, so no XSS is possible here.
  • error.message is rendered in JSX text nodes (auto-escaped). This pattern was present in the code before this PR.
  • The search value is reflected into the page URL as ?q=… through useListState, but is never rendered as raw HTML.
  • No new network requests to caller-supplied URLs (no SSRF surface added).

5. Dependency / supply-chain risk

Result: None.

No new external npm packages are introduced. All changes are first-party code within the monorepo.


Informational: documented limitation, not a vulnerability

When search is active, getAllForms scans at most DEFAULT_MAX_PAGE_SIZE (1000) rows from the DB (sorted createdAt desc). Forms beyond that position are silently excluded from search results. This is a correctness/UX limitation that is correctly documented in a code comment; it does not create a security boundary breach.


Summary: The changes are safe to merge from a security standpoint. The search feature is properly bounded (Zod validation + in-memory filter), authorization hooks are unaffected, and the new client-side access guards follow established patterns in the codebase.

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

@github-actions

Copy link
Copy Markdown
Contributor

Shadcn registry validated — no registry changes detected.

…submit client-side

The resource mutation hook calls the plugin `refresh` override after every
successful mutation. On public form pages that override is a full page
reload, which remounted FormRenderer and wiped the client-side success
screen right after submission (caught by the codegen E2E public-submission
spec). Add `refresh?: boolean` to ResourceMutationDef (default true) and
declare `refresh: false` on the public submit mutation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@olliethedev
olliethedev merged commit 30d2c50 into v3 Aug 18, 2026
9 checks passed
@olliethedev
olliethedev deleted the feat/form-builder-phase2-sweep branch August 18, 2026 23:51
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