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

@flowmonkey/forms

v0.0.1

Published

Secure form submission handling for FlowMonkey workflows

Readme

@flowmonkey/forms

Secure form submission handling for FlowMonkey workflows. Create forms that trigger workflows with built-in security features like CAPTCHA, rate limiting, honeypot fields, and duplicate detection.

Installation

npm install @flowmonkey/forms
# or
pnpm add @flowmonkey/forms

Quick Start

import { FormService, MemoryFormStore, MemorySubmissionStore } from '@flowmonkey/forms';
import { Engine } from '@flowmonkey/core';

// Create stores (use PgFormStore/PgSubmissionStore for production)
const formStore = new MemoryFormStore();
const submissionStore = new MemorySubmissionStore();

// Create form service
const formService = new FormService(formStore, submissionStore, engine);

// Create a form
const form = await formService.createForm({
  name: 'Contact Form',
  flowId: 'contact-workflow',
  contextKey: 'formData',
  fields: [
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'name', type: 'text', label: 'Name', required: true, minLength: 2 },
    { name: 'message', type: 'textarea', label: 'Message', required: true },
  ],
  enabled: true,
  successMessage: 'Thank you for your message!',
});

// Process a submission
const result = await formService.submit(form.id, {
  email: '[email protected]',
  name: 'John Doe',
  message: 'Hello, I have a question...',
}, {
  ip: req.ip,
  userAgent: req.headers['user-agent'],
});

if (result.success) {
  console.log(`Submission ${result.submissionId} triggered execution ${result.executionId}`);
}

Features

Form Fields

Supported field types with validation:

const fields: FormField[] = [
  // Text input
  { name: 'username', type: 'text', label: 'Username', required: true, minLength: 3, maxLength: 20 },
  
  // Email with format validation
  { name: 'email', type: 'email', label: 'Email', required: true },
  
  // Multi-line text
  { name: 'bio', type: 'textarea', label: 'Bio', maxLength: 500, rows: 5 },
  
  // Number with range
  { name: 'age', type: 'number', label: 'Age', min: 18, max: 120 },
  
  // Single select dropdown
  { 
    name: 'country', 
    type: 'select', 
    label: 'Country',
    options: [
      { value: 'us', label: 'United States' },
      { value: 'uk', label: 'United Kingdom' },
    ]
  },
  
  // Checkbox (boolean)
  { name: 'subscribe', type: 'checkbox', label: 'Subscribe to newsletter', defaultValue: false },
  
  // Radio buttons
  {
    name: 'plan',
    type: 'radio',
    label: 'Plan',
    options: [
      { value: 'free', label: 'Free' },
      { value: 'pro', label: 'Pro' },
    ]
  },
  
  // Date picker
  { name: 'birthdate', type: 'date', label: 'Birth Date' },
  
  // File upload
  { name: 'resume', type: 'file', label: 'Resume', accept: ['application/pdf'], maxSize: 5_000_000 },
  
  // Hidden field (pre-filled data)
  { name: 'source', type: 'hidden', defaultValue: 'website' },
];

Security Features

CAPTCHA Verification

Support for reCAPTCHA v2/v3, hCaptcha, and Cloudflare Turnstile:

const form = await formService.createForm({
  // ...fields
  security: {
    captcha: {
      provider: 'recaptcha-v3',
      siteKey: 'your-site-key',
      secretKey: 'your-secret-key',
      minScore: 0.5, // Minimum score threshold (v3 only)
    },
  },
});

// Submit with CAPTCHA token
await formService.submit(formId, data, {
  captchaToken: 'token-from-frontend',
  ip: req.ip,
});

Rate Limiting

Prevent abuse with configurable rate limits:

const form = await formService.createForm({
  // ...fields
  security: {
    rateLimit: {
      maxSubmissions: 5,      // Max submissions per window
      windowSeconds: 3600,    // 1 hour window
      keyBy: 'ip',            // Rate limit by: 'ip', 'fingerprint', 'formId', 'combined'
    },
  },
});

// Requires rate limit store
const formService = new FormService(formStore, submissionStore, engine, {
  rateLimitStore: new MemoryRateLimitStore(), // or PgRateLimitStore
});

Honeypot Fields

Catch bots with invisible honeypot fields:

const form = await formService.createForm({
  // ...fields
  security: {
    honeypot: {
      fieldName: '_hp_field', // Hidden field name
    },
  },
});

In your HTML, add a hidden field that humans won't fill:

<input type="text" name="_hp_field" style="display:none" tabindex="-1" autocomplete="off">

Duplicate Detection

Prevent duplicate submissions within a time window:

const form = await formService.createForm({
  // ...fields
  security: {
    deduplication: {
      enabled: true,
      hashFields: ['email', 'message'], // Fields to hash for comparison
      windowSeconds: 300,               // 5 minute window
    },
  },
});

// Requires deduplication store
const formService = new FormService(formStore, submissionStore, engine, {
  deduplicationStore: new MemoryDeduplicationStore(), // or PgDeduplicationStore
});

Multi-Tenancy

Isolate forms by tenant:

const form = await formService.createForm({
  name: 'Contact Form',
  tenantId: 'tenant-123',
  // ...
});

// List forms for a tenant
const tenantForms = await formService.listForms({ tenantId: 'tenant-123' });

Events

Listen to form lifecycle events:

formService.on('form:created', ({ formId, name }) => {
  console.log(`Form ${name} created with ID ${formId}`);
});

formService.on('submission', ({ formId, submissionId, status }) => {
  console.log(`New submission ${submissionId} for form ${formId}`);
});

formService.on('completed', ({ formId, submissionId, executionId, durationMs }) => {
  console.log(`Submission ${submissionId} completed in ${durationMs}ms`);
});

formService.on('failed', ({ formId, submissionId, errorCode, message }) => {
  console.error(`Submission ${submissionId} failed: ${errorCode}`);
});

Route Definitions

Use the pre-defined routes for your API:

import { FormRoutes, buildFormRoute } from '@flowmonkey/forms';

// Example with Express
app.get(FormRoutes.ListForms, async (req, res) => {
  const forms = await formService.listForms();
  res.json({ success: true, data: forms });
});

app.post(FormRoutes.SubmitForm, async (req, res) => {
  const { formId } = req.params;
  const result = await formService.submit(formId, req.body, {
    ip: req.ip,
    userAgent: req.headers['user-agent'],
    captchaToken: req.body._captcha,
  });
  res.json(result);
});

// Build routes with parameters
const url = buildFormRoute(FormRoutes.GetForm, { formId: 'contact-form' });
// => '/api/forms/contact-form'

Public Form Data

Get form schema for frontend rendering (without secrets):

import { toPublicFormData } from '@flowmonkey/forms';

app.get('/forms/:formId', async (req, res) => {
  const form = await formService.getForm(req.params.formId);
  if (!form || !form.enabled) {
    return res.status(404).json({ error: 'Form not found' });
  }
  
  // Returns fields, captcha site key (not secret), honeypot field name, etc.
  res.json(toPublicFormData(form));
});

Database Schema

For production, use the PostgreSQL stores:

import { Pool } from 'pg';
import {
  applyFormSchema,
  PgFormStore,
  PgSubmissionStore,
  PgRateLimitStore,
  PgDeduplicationStore,
} from '@flowmonkey/forms';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Apply schema (run on startup or in migrations)
await applyFormSchema(pool);

// Create stores
const formStore = new PgFormStore(pool);
const submissionStore = new PgSubmissionStore(pool);
const rateLimitStore = new PgRateLimitStore(pool);
const deduplicationStore = new PgDeduplicationStore(pool);

// Create service
const formService = new FormService(formStore, submissionStore, engine, {
  rateLimitStore,
  deduplicationStore,
});

API Reference

FormService

| Method | Description | |--------|-------------| | createForm(input) | Create a new form definition | | getForm(id) | Get form by ID | | updateForm(id, updates) | Update form definition | | deleteForm(id) | Delete a form | | listForms(filter?) | List forms with optional filtering | | submit(formId, data, meta) | Process a form submission | | getSubmission(id) | Get submission by ID | | listSubmissions(filter?) | List submissions with filtering | | countSubmissions(filter?) | Count submissions matching filter |

Validation

| Function | Description | |----------|-------------| | validateSubmission(form, data) | Validate form data against fields | | buildSchemaFromFields(fields) | Build JSON Schema from field definitions | | checkHoneypot(data, fieldName) | Check if honeypot field was filled (spam) | | computeSubmissionHash(data, fields) | Compute hash for deduplication | | applyDefaults(fields, data) | Apply default values to submission | | sanitizeSubmission(data, honeypot?) | Remove honeypot field from data |

CAPTCHA

| Function | Description | |----------|-------------| | verifyCaptcha(config, token, ip?) | Verify CAPTCHA token | | createCaptchaProvider(config) | Create provider instance |

License

MIT