npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formbuilder-cloud/adapter

v0.1.1

Published

Connect formRender (formBuilder 3.23.1+) to formBuilder Cloud: render a published form or attach to an existing one, submit with an Idempotency-Key and safe retries, and show every documented outcome. No telemetry.

Readme

@formbuilder-cloud/adapter

Connects formRender (formBuilder 3.23.1 and later 3.x) to formBuilder Cloud. It renders a published form, or attaches to a form you already render. It then submits the values formRender's userData produces, with an Idempotency-Key and safe retries, and shows every documented outcome. It is a separate, opt-in package: formBuilder and formRender are unchanged and never call Cloud themselves, and removing the adapter leaves your local formRender working exactly as before.

  • No telemetry. Importing the package or loading the script makes no request. After render() or attach() it requests only GET /v1/forms/{f}/published and POST to that document's endpoint.url, both on apiBase. It loads Cloudflare Turnstile only when the form's owner turned the challenge on.
  • Collection keys only. key must be the form's public collection key (fb_col_…). A management key or a JWT throws CloudConfigError before any request: management credentials must never be used in a browser.
  • Version 0.1.0, not published yet. The npm org exists only once Kevin creates it. Until the first release, the script-tag snippets keep their <VERSION> and <SRI> placeholders.

Builds

| File | Use | Notes | |---|---|---| | dist/index.js (+ dist/index.d.ts) | bundlers (ESM) | needs jquery and formBuilder (peer dependencies) and window.jQuery set | | dist/cloud.min.js | a page that already loads jQuery and formRender | IIFE, sets only window.FormBuilderCloud; at most 10 KB gzip (checked in CI) | | dist/embed.min.js | a page with nothing on it | the same API plus the unmodified jQuery 3.7.1 and formRender 3.23.1, which keep their MIT notices (THIRD-PARTY-NOTICES.md); it defines window.jQuery/$ only when the page has none |

Script tag

The dashboard's embed tab, GET /v1/forms/{f}/embed and the MCP get_embed_snippet tool all serve the same snippet (S-11 S11-19, shared/src/snippets.ts):

<div id="fb-form"></div>
<script src="https://cdn.jsdelivr.net/npm/@formbuilder-cloud/adapter@<VERSION>/dist/cloud.min.js" integrity="<SRI>" crossorigin="anonymous"></script>
<script>FormBuilderCloud.render({ el: '#fb-form', formId: '<FORM_ID>', key: '<YOUR_fb_col_KEY>' })</script>
<noscript>This form needs JavaScript.</noscript>

Always pin an exact version and its SRI hash. If you swap the src for …/dist/embed.min.js, use that file's hash: an integrity value belongs to one file. Both hashes are printed at release (RELEASING.md).

npm

import $ from 'jquery'
import { render } from '@formbuilder-cloud/adapter'

;(window as unknown as { jQuery: unknown; $: unknown }).jQuery = (window as unknown as { $: unknown }).$ = $
await import('formBuilder/dist/form-render.min.js')

const form = await render({
  el: '#fb-form',
  formId: '<FORM_ID>',
  key: '<YOUR_fb_col_KEY>',
  onAccepted: ({ id, replayed }) => console.log('stored', id, replayed),
})
console.log(form.state) // 'ready'

An existing formRender installation

Keep your own formRender() call and markup, and add attach() (S11-20). First import and publish the same definition in Cloud (dashboard "Import JSON", or PUT /draft with a management key from a server). Field names are the identity, and attach() warns in the console when your local names and the published version's differ.

import { attach, type FormRenderInstance } from '@formbuilder-cloud/adapter'

declare const $: (selector: string) => { formRender(options: { formData: unknown[] }): FormRenderInstance }
declare const myDefinition: unknown[]

const fr = $('#my-fields').formRender({ formData: myDefinition }) // your unchanged code
await attach({ form: document.getElementById('my-form') as HTMLFormElement, formRender: fr, formId: '<FORM_ID>', key: '<YOUR_fb_col_KEY>' })

attach() leaves your markup and styles alone: no class and no theme unless you pass theme: 'published' or tokens. Call formRender once per form: an instance that rendered several containers is refused, because formRender renames the later containers' fields.

Without the adapter (plain HTTP)

The adapter is optional. examples/plain-fetch.html and the curl snippet in shared/src/snippets.ts send the same body with fetch or curl: { values, meta: { form_version, client } } with an Idempotency-Key header. Reuse the exact key and the exact body on a retry to get the original 201 with Idempotent-Replayed: true.

API

import { createClient, collectValues, CloudProblem, CloudConfigError, DEFAULT_MESSAGES, VERSION } from '@formbuilder-cloud/adapter'
import type { RenderOptions, AttachOptions, CloudForm, CloudClient, State, Messages, ThemeTokens } from '@formbuilder-cloud/adapter'

const client: CloudClient = createClient({ formId: '<FORM_ID>', key: '<YOUR_fb_col_KEY>' })
const doc = await client.published()
const result = await client.submit({ values: { email: ['[email protected]'] }, meta: { form_version: doc.version } }, { idempotencyKey: crypto.randomUUID() })
console.log(result.replayed, VERSION, DEFAULT_MESSAGES.success, collectValues, CloudProblem, CloudConfigError)

type Uses = [RenderOptions, AttachOptions, CloudForm, State, Messages, ThemeTokens]
  • render({ el, formId, key, apiBase?, messages?, idempotency?, formRenderOptions?, submitLabel?, afterAccepted?, theme?, onAccepted?, onError?, onStateChange? }) creates <form class="fb-cloud" novalidate> inside el. It renders one container with $(container).formRender({ ...formRenderOptions, formData: <deep copy>, dataType: 'json' }) and appends a submit button when the definition has none. afterAccepted is 'message' (default: the message replaces the form), 'reset' or 'keep'.
  • attach({ form, formRender, formId, key, formVersion?, theme?, … }) does the same for your own form. Without formVersion, the server validates against the active version.
  • CloudForm: state, submit() (resolves with the 201 body plus replayed, rejects with CloudProblem), reload() and destroy(). destroy() removes the adapter's nodes, listeners, theme and sessionStorage entry.
  • collectValues(instance, { quill? }) returns formRender's userData as name → string[], verbatim. It also reads Quill fields, which formRender's getter never includes.
  • createClient({ formId, key, apiBase? }) gives published() and submit(body, { idempotencyKey }), with the same retries.
  • CloudProblem has status, type (an API problem type, 'network' or 'unknown'), detail, errors, retryAfter and requestId. CloudConfigError is thrown synchronously for a bad option.

States and messages

loading, ready, submitting, accepted, invalid, stale, closed, unavailable and error. Success is shown only after a 201, and it says "received", never "delivered". Values are never cleared on a failure. Every string is in messages (DEFAULT_MESSAGES); unavailable takes {minutes}. The adapter adds only the classes fb-cloud, fb-cloud__message, fb-cloud__field-error and fb-cloud__summary.

| Answer | What the respondent sees | |---|---| | 201 (a replay too) | the success message; onAccepted | | 400 validation-failed | a message next to each field (aria-invalid, aria-describedby, setCustomValidity), a summary (role="alert") for fields that are not on the page, and focus on the first invalid field; editing a field clears its error | | 400 rejected-spam | "Your response could not be submitted." | | 400 invalid-body / invalid-idempotency-key, 415 | a generic error; the console names the problem type (an integration bug) | | 401, 403, 404 | "temporarily unavailable"; the console names the form and the key's first 12 characters | | 403 origin-not-allowed | "temporarily unavailable"; the console asks you to add location.origin to the form's allowed origins | | 409 idempotency-conflict | "Please submit again." (a new key next time) | | 410 version-retired / version-unknown | the new version is fetched and rendered with the answers kept; nothing is submitted automatically | | 410 form-archived / workspace-suspended, 429 quota-exceeded | "This form is no longer accepting responses." | | 410 collection-paused, 429 rate-limited over 60 s | "Please try again in N minutes." | | 413 | "Your response is too large." | | network, 500, 502, 503, 504 | up to 3 automatic retries (1, 3, 9 s ±20 %, at least Retry-After, at most 30 s), then "We couldn't send your response" + Try again |

Idempotency and retries

One key per logical submission, i.e. per canonical (sorted-key) body: retries, a double click and a page reload reuse it, and a changed answer gets a new one. With idempotency: 'session' (the default) sessionStorage keeps { key, bodySha256, createdAt } under fb-cloud:idem:<formId> for at most 23 hours; values are never stored. On an insecure page (no crypto.subtle) the key lives in memory. A retry resends identical bytes, so the server replays its first 201 instead of answering 409. A 429 rate-limited with Retry-After of 60 s or less is retried once. Nothing is queued offline.

Themes

With theme: 'published' (the default for render()), the adapter applies the form's published theme tokens (document.theme, issue #19). It first checks every token with the same validator as the API (hex colours, integers in range and fixed enums only). It then sets them only through style.setProperty('--fbc-…') on its own form, and inserts one constant stylesheet whose selectors are all under .fb-cloud[data-fbc-theme]. If any token is invalid or unknown, the whole theme is ignored with a console.warn, and formRender's own look is kept. 'none' turns themes off; a ThemeTokens object applies your own. No theme ever adds a logo, a badge or a link. The Turnstile widget is dark on a dark background.

Turnstile

When the form's owner enabled the challenge, the adapter loads https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit once and renders the widget before the submit button. It sends the token as meta.challenge_token, and resets the widget after a 201 and after any 4xx answer, because a verified token is spent even when the submission then fails.

Content Security Policy

  • connect-src https://api.formbuilder.online (your apiBase);
  • script-src for the adapter's and formRender's origins (jsDelivr or your own host);
  • style-src 'unsafe-inline', because formRender injects style tags (themes need nothing more);
  • with Turnstile: script-src https://challenges.cloudflare.com and frame-src https://challenges.cloudflare.com;
  • the CDN origins of formRender's rich-text and plugin controls, only if you use them (Quill: cdn.quilljs.com; TinyMCE, starRating, Trumbowyg: cdnjs.cloudflare.com);
  • formRender's translations: on its first render, formRender fetches <i18n.location>en-US.lang (default assets/lang/, relative to the page). That is library behaviour, harmless when it fails. Allow it with connect-src 'self', or set formRenderOptions.i18n.location to a URL you serve.

Support

  • formRender 3.23.1 (adapter/test/versions.json; peer range >=3.23.1 <4), jQuery 3.7.
  • The last two versions of Chrome, Edge, Firefox and Safari, and iOS Safari 16+. CI runs the example pages in Chromium.
  • No React or Vue wrapper packages. The React, Vue and Next.js templates (npm create @formbuilder-cloud, examples/ in the formbuilder-cloud repository) show the component around render() and destroy().

Leaving Cloud

Remove the script tag or the import. Export your definitions (their definition loads straight into formData) and your submissions. Then post $(…).formRender('userData') to your own endpoint. The adapter leaves nothing behind: no cookies, no other globals, and its sessionStorage entry goes after a 201, after 23 hours, or on destroy().

Licence

UNLICENSED: proprietary. Copyright (c) 2026 Kevin Chappell, all rights reserved. You may use it only with the formBuilder Cloud service and under that service's terms (LICENSE). dist/embed.min.js also contains jQuery and formRender, which stay under their own MIT licences. Their notices are in THIRD-PARTY-NOTICES.md and at the top of that file.