Hardening formseal-embed: tightening trust boundaries and operator responsibility

Just shipped v3.6.0 and v3.7.0 - two releases focused on hardening the browser runtime.


Runtime freeze layer

The biggest change in v3.6.0 is a new runtime hardening layer. I added fse.freeze.js - a bootstrap script that runs after all modules load and locks down the internal state:

  • Deep-freezes the config copy (endpoint, public key, field definitions)
  • Freezes all runtime modules (crypto, payload, validation, form)
  • Locks the __fse_internal__ namespace as non-writable, non-configurable

After initialization, this fails:

window.__fse_internal__.FSE.publicKey = "attacker-key"
// TypeError: cannot assign to frozen object

[!NOTE] This isn't bulletproof against a fully compromised browser - that's not the threat it's designed for. It prevents accidental mutation and sloppy interference from third-party scripts, analytics, and compromised tag managers, which is the realistic concern.


HTTPS enforcement

formseal-embed now refuses to run on HTTP. The crypto module checks window.isSecureContext and throws if you're not on HTTPS - with a localhost exemption for development:

if (!window.isSecureContext) {
  if (!(loc.hostname === "localhost" || loc.hostname === "127.0.0.1")) {
    throw new Error("[fse/crypto] Secure context (HTTPS) required.");
  }
}

No more accidental plaintext submissions on insecure sites.


Removed honeypot

The built-in honeypot field (_hp) is gone. Spam protection is a separate concern from encryption, and bundling them together meant the crypto layer was making assumptions it had no business making. Developers handle their own spam protection (honeypot, CAPTCHA, whatever) if they want it.


Fixed a lying warning

There was a bug where the validation warning said:

[fse/validate] Field 'name' exceeds maxLength (25 > 10). Value truncated to 10 chars before encryption.

We weren't actually truncating anything - we were blocking the submission entirely. Fixed:

[fse/validate] Field 'name' exceeds maxLength (25 > 10). Submission blocked.

New fse keygen command

v3.7.0 adds a one-liner for key generation:

fse keygen

Output:

{
  "publicKey": "jLPrZQgckIRUecjSaFfbfutHDL4D9hzM1JoXU-DliT4",
  "privateKey": "yOKv1BmkjnI7WvQNj-ZAwqMOfsrSVBZJ8abkmHN2wiA"
}

Keep this keypair somewhere safe. Never store it on your server. Loss of the private key means permanent loss of decryption ability - there is no recovery path.

Previously the docs had a Python snippet you had to copy-paste and run manually. Now it's a one-liner.


Summary

These releases are about hardening the trust boundary:

  • Runtime freeze prevents late-stage mutation
  • HTTPS enforcement stops insecure deployments
  • Honeypot removal keeps concerns separated
  • The validation warning now says what actually happens
  • fse keygen replaces the copy-paste setup snippet

The project is at v3.7.0 now. Full changelog on GitHub.