Repository navigation
release: public API v1 with API keys, MCP section, public form survives deploys - #238
Merged
Merged
Conversation
The web builds every form link with publicFormPath. The API is about to hand the same link to agents, so the rule moves to @quill/shared (with an editor path and a formLinks helper that joins both onto PUBLIC_APP_URL), and the web module re-exports it instead of keeping its own copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
api_key.member_id with an (account_id, member_id) index, and form.created_via, form.previous_config and form.previous_published_at_ms, in SQLite and Postgres and in both Drizzle schemas. Purely additive: an upgraded database keeps every row and the new columns read NULL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
getAccountPublicCode returns the vanity slug when claimed, else the short code: the first segment of a form link, as the dashboard builds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Create, get, list (each item), update, slug, duplicate and publish now carry the form's public link and its builder link, built with the shared publicFormPath on PUBLIC_APP_URL and the workspace's canonical code. Both are null when PUBLIC_APP_URL is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
…ncipal HostPrincipal gains via: 'session' | 'api_key' and an optional apiKeyId. AuthService.resolveHost only resolves sessions today, so it always returns via: 'session' and otherwise the same principal as before. isApiKeyPrincipal is the one check the key, strict validation and created-via metric code will use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
… new specs The upgrade test now migrates through 0024 only, so a later migration cannot change what it asserts. Both new spec files follow the shared Prettier preset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Adds admin.chrome.nav.developers, admin.developers (the API keys section: list, empty and error states, create dialog with the one-time reveal, revoke confirmation, KEY_LIMIT message) and admin.developerDocs (the docs placeholder), in English and Spanish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Between Integrations and Docs, in the desktop rail (open and collapsed) and the mobile drawer, active on /admin/developers and below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Client methods for GET/POST /v1/api-keys and DELETE /v1/api-keys/:id, built against the spec contract, and the two server actions the page calls through callAction. Refusals come back as stable codes (INVALID, KEY_LIMIT, FORBIDDEN, FAILED); a 404 on revoke counts as already revoked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
/admin/developers lists the caller's keys (all of the workspace's, with who created each, for owners and admins), creates a key in a dialog that shows the full key once with a copy button and a warning, and revokes with confirmation. Empty and error states, no horizontal scroll on a phone. The docs section is a placeholder component, DeveloperDocs in developer-docs.tsx, that the docs ticket replaces as a unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
…STALE A guarded PUT or publish that loses the optimistic lock answers 409 with the form as it is now. That `current` was masked but had no links, so it was the one form in the admin API without `publicUrl`/`editorUrl`. `unwrapGuarded` now takes the controller's `formView` to shape it like every other response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
A "use server" file may export async functions only, so the key name limit moves to its own module, and the client panel imports only types from admin-api (the module reads the session cookie and cannot reach a client bundle). vitest stubs both, so only the production build saw it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
A dependency-free ZIP writer (deflate or store, UTF-8 names) for the skill archive, and publicApiUrl: the API address a customer can reach, read at runtime from PUBLIC_API_URL, never a cluster-internal API_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Generated from formConfigSchema with zod-to-json-schema (zod 3), every sub-schema inlined, minus the fields the editor has no control for. ajv (dev only) checks it is a valid draft-07 schema that accepts the recipe configs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
SKILL.md with its rules, the endpoint and error references, the config schema, five recipes and INSTALL.md for Claude Code, Codex and Cursor. The API and dashboard URLs are parameters, injected per request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
GET /api/developers/skill confirms the session with the API (401 without one), then builds the zip with this environment's public API address and dashboard origin. PUBLIC_API_URL is documented in .env.example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
DeveloperDocs shows the runtime API URL, Bearer auth, the create, edit, publish and revert flow, the traps, and the skill download, in English and Spanish from its own catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
- A 404 on revoke is no longer reported as a revoke. The API answers 404 for a key of another workspace too, which happens when the workspace changed in another tab; the page now says the key is no longer in this workspace and refreshes the list (GONE, new catalog string EN + ES). - The create dialog stays open while the key is being created, so Escape or a backdrop click cannot create a key whose secret is never shown. - Overlapping revokes no longer clear each other's busy state. - The page reads /v1/me and the key list in parallel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
- The skill route refreshes an expired access token once before answering
401, like hostFetch, so a download clicked long after the page loaded
works for a member who is still signed in. A 403 from the API (workspace
no longer theirs) answers 403 instead of a 502.
- DeveloperDocs takes the optional locale prop the Developers page already
passes (<DeveloperDocs locale={locale} />), so the two branches merge
without a type error.
- publicApiUrl treats private and link-local addresses as internal, so a
Docker or VPC API_URL is never printed to customers.
- The skill docs name every role gate a member's key hits (notifications,
brand kit apply and revert, webhook test) and list forms in the order
the API returns them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
…I_URL When API_URL is cluster-internal, fall back to NEXT_PUBLIC_API_URL read from the runtime environment instead of reporting the address as unconfigured. Deployments that already set it per environment need no new variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
feat(api): add API v1 columns (migration 0024) and principal via
feat(api): return publicUrl and editorUrl on v1 form responses
feat(web): Developers page to manage API keys
# Conflicts: # apps/web/app/admin/developers/developer-docs.tsx
The "How to use the API" section read its copy from a local catalog next to the component. All UI copy belongs in the shared catalog, so it now lives in admin.developerDocs, replacing the placeholder keys from the Developers page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Answer /admin/developers with a real HTTP 307 from next.config (the page redirects too), and give API docs its own icon (pi-file-code, added to the icon sheet) in the flyout and on the keys page link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
The page needs a few strings apiDocs does not carry: the Authentication section, the Getting started intro worded for this page, the per-area endpoint count, the empty search hint and the copy, fold and CSV labels of the endpoint cards. Kept in their own apiDocsPage block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
/admin/developers/docs renders every route an API key may call from the
typed catalog: a sticky index (a selector on small screens), Getting
started with the base URL, the create, edit, publish and revert flow,
the gotchas and the skill download, Authentication, Errors with the
example 400 and every code, Limits, the reference area by area and what
a key cannot do.
Each endpoint card carries its method badge, the {param} path, the
prose, a parameters table grouped by path, query and body, the curl
call against this environment's public API URL with $DFORMS_API_KEY
and a copy button, the example answer (folded when long) and its own
anchor. A search filters endpoints by path, method or description,
ignoring case and accents, with an empty state. Deep links scroll into
place even though the admin layout streams the page in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
… reads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
feat(web): API docs page inside Developers, with a submenu
The public form reached its server through Server Actions, called by an id compiled into each build. A deploy ships new ids, so a form opened before it could not be finished: the request in flight hit the old pod (504) and every retry hit the new one with an unknown id (404). Submit (partial and complete), funnel events, the booking record and the upload presign now go to one same-origin route with a stable address, POST /api/public/forms/:accountCode/:slug/:op, which relays to the public API exactly as the actions did (X-Forwarded-For included) and answers every verdict with a 200. Both layouts and embeds use it; the builder preview's fence blocks it like any same-origin write. Kept: the submit retries only on transport failures and stays one response per session; the booking record still goes in a single attempt. Answers in progress are kept in sessionStorage beside the session id, so a reload restores them (slides also return to the current question) and finishes as the same response. Released with the session on a confirmed complete; never kept or restored in the builder preview. Refs Dapta-Tech/dapta-forms-landing#66 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
A name step stores firstname and lastname, never name, so [name] rendered
empty on every form that asked for a name with the name type. [name] now
resolves to firstname + lastname (one space, none when a half is missing)
wherever answers are recalled; an answer stored under `name` itself still
wins, so forms whose own step is keyed `name` read as before.
- engine: fullNameToken, tokenText, respondentFullName and interpolateUrl;
interpolate uses them, and resolveEnding fills the redirect URL tokens
(percent-encoded) when given the answers.
- public form: questions, descriptions, outcome heading and text, the
thank-you screen and the redirect URL (immediate, delayed and after a
booking) in both layouts.
- editor: the @ picker offers [name] after a name step; the scheduler
autofill keeps listing answer keys only.
- emails: a {{respondentName}} token, filled by the API from the
submission, with a chip and a sample value in both email editors.
- copy: redirect URL hints mention placeholders (en, es).
Refs Dapta-Tech/dapta-forms-landing#65
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
- SKILL.md: a section on order, show/hide conditions, @score (the running score of the steps shown before, not counting the step itself; a gated step goes after the last scored question), [firstname], [lastname] and [name], and the {{token}} markers of the notification emails. - form-config.schema.json: the condition `field` description names @score. - New recipe gate-a-step-by-score.md: a booking step hidden below 14 points, its config validated against the schema and the zod contract. - The responses example stores firstname and lastname, as a name step does. Refs Dapta-Tech/dapta-forms-landing#65 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Refs Dapta-Tech/dapta-forms-landing#65 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
…ill on API keys The rail's Developers item reads MCP (EN and ES) in the wide rail, the collapsed flyout and the phone drawer. Its icon is the Claude, ChatGPT and Cursor logos taking turns every 2.6s as currentColor masks, paused under the pointer and still with reduced motion. Addresses stay under /admin/developers. The API keys page is titled MCP and offers the skill zip after the keys. The skill and the OpenAPI description name MCP, then API keys. Refs Dapta-Tech/dapta-forms-landing#69 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
resolveOptionIcon returns { kind: 'none' } for an option with no icon
(absent, empty or blank) instead of the label's initials. The public and
embedded form, the live preview and the builder canvas draw such a card as
its centred label, at the height of a one-line icon card so mixed rows
stay even. The icon picker shows an empty slot instead of initials. Lists,
emoji, letters and logos are unchanged.
Refs Dapta-Tech/dapta-forms-landing#69
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
- An API 5xx on submit (a proxy in front of a rolling API) is no longer handed to the browser as a refusal: the route answers 502 and the browser retries, which is safe because the submit is idempotent. - The route counts the body while it streams and stops at the ceiling, instead of buffering a body that omits or understates its length. - A restored draft beats a slider's mount-time default (child effects run first), and in the one-page layout a moved slider is no longer marked as seeded after a reload. - "Already started" is stored in the draft explicitly, so a visitor who only loaded and reloaded still sends `start` and does not warm the human check. - @quill/db: a partial's update carries `completed_at IS NULL`, so a partial that read an open row can no longer overwrite a complete that committed in between (requests from one tab now run in parallel). Refs Dapta-Tech/dapta-forms-landing#66 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
The Claude mark keeps its original orange (#d97757, the color Dapta's admin panel uses for it) in every rail state and both themes. The monochrome ChatGPT and Cursor marks stay on currentColor. Refs Dapta-Tech/dapta-forms-landing#69 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Josue Hernandez <josue@daptatech.com>
Decision on the ticket: only the form's own page fills [key] tokens into
the redirect URL. When the form runs embedded (in a frame, or loaded with
?embed=1), every query parameter that carries a token is dropped whole and
the rest of the URL is kept, so neither the host page (postMessage) nor
the destination (top.location) receives an answer.
- engine: stripUrlTokens and redirectUrlFor; resolveEnding takes
{ embedded } as a fourth argument.
- web: isEmbeddedRender (frame or ?embed=1) decides it on the immediate,
delayed and post-booking redirects of both layouts.
- top-navigate: the route 1 security note holds again (no answers ever
reach the host page), so it is restored.
- copy: the redirect hints (en, es) and SKILL.md say an embedded form
does not send placeholders.
Refs Dapta-Tech/dapta-forms-landing#65
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Josue Hernandez <josue@daptatech.com>
feat: [name] as the full name, and @score in the API skill
Signed-off-by: Josue Hernandez <josue@daptatech.com>
The spec from #235 mocked ./actions, which this branch replaces with ./transport. Same functions and argument positions, renamed. Signed-off-by: Josue Hernandez <josue@daptatech.com>
…ploy fix(web): public form submit survives a deploy
feat: MCP menu with rotating logos, skill download on API keys, cards without initials
CodeQL flagged /\/+$/ and /^-+|-+$/ as polynomial ReDoS in form-links and the API reference. Their inputs are server config and fixed route strings, so nothing was exploitable, but the loops are just as short. Signed-off-by: Josue Hernandez <josue@daptatech.com>
fix(shared): trim with a linear loop instead of a backtracking regex
feggome1
approved these changes
Oct 15, 2026
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.
Public API v1 with API keys, the MCP section in the admin, and a public-form fix. 13 PRs since #221.
developis19e0cbc.KEY_NOT_ALLOWED.publicUrlandeditorUrlon v1 form responses.@score, screen order, placeholders.[name]resolves to the full name, also in redirects and emails ({{respondentName}}). An embedded form's redirect never carries answers.Deploy notes
0024_api_v1(Postgres and SQLite): runs on its own in the init container.PUBLIC_API_URLon prd forms-web to the prd public API origin. The API keys page, the docs page and the skill zip show the API address agents call. Without it, the web falls back toNEXT_PUBLIC_API_URL, which the shared image inlines with the dev API, so prd users would be told to call dev with prd keys (401).Signed-off-by: Josue Hernandez josue@daptatech.com