Package Reference
cli
import "github.com/frob/webform-relay/cmd/cli"Index
submit
import "github.com/frob/webform-relay/cmd/submit"Index
captcha
import "github.com/frob/webform-relay/internal/captcha"Index
func TokenField
func TokenField(provider string) stringTokenField returns the default HTML form field name for a provider’s client token.
type Verifier
Verifier checks a captcha token against a provider’s verification API.
type Verifier interface {
Verify(ctx context.Context, token string) error
}func New
func New(cfg config.Captcha, client *http.Client) VerifierNew returns a Verifier that validates tokens against the provider’s public endpoint.
func NewWithEndpoint
func NewWithEndpoint(cfg config.Captcha, client *http.Client, endpoint string) VerifierNewWithEndpoint returns a Verifier pointing at a custom endpoint URL. Used in tests to target a local httptest.Server.
cli
import "github.com/frob/webform-relay/internal/cli"Index
func Run
func Run(args []string, stdout, stderr io.Writer) intRun executes the CLI. args is os.Args (args[0] is program name). stdout and stderr are injectable for testing. Returns the exit code (0=success, 1=error, 2=usage error).
config
import "github.com/frob/webform-relay/internal/config"Index
- func Schema() ([]byte, error)
- type Captcha
- type Condition
- type Config
- type Constraints
- type Field
- type FileLoader
- type Form
- type InlineLoader
- type Loader
- type Option
- type Output
- type S3Client
- type S3Loader
- type ShowWhen
- type Spam
- type TTLCache
func Schema
func Schema() ([]byte, error)Schema returns the JSON Schema for the configuration format, for editor completion and inline validation (S9).
The schema is derived directly from the Go types in this package (Config, Form, Field, Option, Constraints, Output) and their validity maps (validFieldTypes, validCaptchaProviders, validOutputTypes, reservedFieldNames), so it stays in sync with what Parse actually accepts. The output is deterministic: struct fields marshal in declaration order and map keys are sorted alphabetically by encoding/json, so two calls to Schema() always produce byte-identical output.
type Captcha
Captcha holds the captcha/spam-prevention config for a form.
type Captcha struct {
Provider string
Secret string
MinScore float64
Field string // overrides the default token field name for the provider
}type Condition
Condition is a single conditional-visibility predicate: field OP value.
Operator is one of “equals”, “not_equals”, “one_of”, “not_one_of”, “filled”, or “empty”. Value carries the scalar for equals/not_equals; Values carries the sequence for one_of/not_one_of; filled/empty carry neither.
type Condition struct {
Field string
Operator string
Value string
Values []string
}type Config
Config holds the top-level configuration.
The returned Config must not be modified by callers.
type Config struct {
CacheTTL time.Duration
Forms map[string]Form
}func Parse
func Parse(data []byte) (*Config, error)Parse parses YAML config data, applies defaults, and validates the result.
func (*Config) RequiresTokenSecret
func (c *Config) RequiresTokenSecret() boolRequiresTokenSecret reports whether any form in c enables the min-fill token check (Spam.MinFillSeconds > 0), meaning the deployment needs a token-signing secret configured.
type Constraints
Constraints holds the server-side validation rules for a field (S4). Pointer fields distinguish an unset constraint from a zero value.
type Constraints struct {
MinLength *int
MaxLength *int
Min string
Max string
Step string
Pattern string
OneOf []string
MinSelected *int
MaxSelected *int
MatchesField string
DiffersFrom string
}type Field
Field describes one form field.
The properties beyond Name/Required/Type are the v1 additions. They are declared here so tests and downstream packages can compile against the full model; wiring them through parsing, validation, and rendering happens in the later phases of the Form Config v1 plan.
type Field struct {
Name string
Required bool
Type string
// Common presentational properties (S2.1).
Label string
LabelHidden bool
Help string
Hint string
HintPosition string
Placeholder string
Default string
Disabled bool
Readonly bool
Hidden bool
Autocomplete string
Attributes map[string]string
// Content carries the verbatim HTML for a static `type: html` element.
Content string
// Prefix/Suffix are verbatim strings emitted immediately before/after
// this element's rendered HTML, applicable to any element type.
//
// Scaffolding only: declared so tests compile. Parse-time wiring and
// renderer wiring are added by the implementation phase.
Prefix string
Suffix string
// Wrapper is a Go text/template string that wraps this element's
// rendered HTML: composition is Prefix + wrapper(rendered element) +
// Suffix. Template data is {{ .Element }} (the rendered HTML, emitted
// verbatim) and {{ .Field }} (this Field's own metadata).
//
// Scaffolding only: declared so tests compile. Parse-time syntax
// validation and renderer wiring are added by the implementation phase.
Wrapper string
// Type-specific properties (S2.2).
Rows int
Min string
Max string
Step string
Multiple bool
EmptyLabel string
Inline bool
Options []Option
// Validation (S4).
Validate Constraints
Messages map[string]string
// ShowWhen carries the parsed conditional-visibility clause for this
// element (nil = always visible).
//
// Scaffolding only: declared so tests compile. Parse-time wiring and
// validation are added by the implementation phase; Parse leaves this
// nil for now.
ShowWhen *ShowWhen
}type FileLoader
FileLoader loads config from a local file path. Intended for local development and end-to-end testing only.
type FileLoader struct {
Path string
}func (FileLoader) Load
func (l FileLoader) Load(_ context.Context) ([]byte, error)type Form
Form represents a single webform configuration.
type Form struct {
Captcha Captcha
Spam Spam
Fields []Field
Outputs []Output
// Confirmation is the message shown in the HTML confirmation fragment
// returned to htmx requests on success (and on silent spam drops).
Confirmation string
}type InlineLoader
InlineLoader returns config from bytes already held in memory. Intended for local development and end-to-end testing only.
type InlineLoader struct {
Data []byte
}func (InlineLoader) Load
func (l InlineLoader) Load(_ context.Context) ([]byte, error)type Loader
Loader is the interface for loading raw config bytes.
type Loader interface {
Load(ctx context.Context) ([]byte, error)
}type Option
Option is one choice in a select/radios/checkboxes field (S3).
type Option struct {
Value string
Label string
Selected bool
Disabled bool
}type Output
Output describes a destination for submitted form data.
type Output struct {
Type string
URL string
To string
Subject string
Headers map[string]string
Mapping map[string]string
Static map[string]string
Host string
Port string
Username string
Password string
// OnError is the per-output failure policy ("fail" | "continue").
// Phase 3 scaffolding: declared so tests compile. Parse-time
// normalization/validation is added by the implementation phase.
OnError string
}type S3Client
S3Client is the subset of the S3 client API used by S3Loader.
type S3Client interface {
GetObject(ctx context.Context, in *s3.GetObjectInput, opts ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}type S3Loader
S3Loader loads config from an S3 object.
type S3Loader struct {
Bucket string
Key string
Client S3Client
}func (S3Loader) Load
func (l S3Loader) Load(ctx context.Context) ([]byte, error)Load fetches the S3 object and returns its body as bytes. It returns an error if the object body exceeds maxConfigSize bytes.
type ShowWhen
ShowWhen is a parsed `show_when` clause: a set of Conditions combined with either AND (Any == false, the default) or OR (Any == true). A single condition parses to one Condition with Any == false.
type ShowWhen struct {
Any bool
Conditions []Condition
}type Spam
Spam holds the spam-trap config for a form (Phase 4, and the min-fill token check added afterward).
Min-fill scaffolding: MinFillSeconds/FormTTL/TokenField are declared so tests compile. Parse-time wiring (defaults, validation) into Form.Spam is added by the implementation phase.
type Spam struct {
Honeypot string
MinFillSeconds int
FormTTL time.Duration
TokenField string
}type TTLCache
TTLCache wraps a Loader with a time-based cache.
type TTLCache struct {
Loader Loader
TTL time.Duration
// contains filtered or unexported fields
}func (*TTLCache) Get
func (c *TTLCache) Get(ctx context.Context) (*Config, error)Get returns the cached Config if within TTL, otherwise fetches, parses, and caches it. If TTL <= 0 the cache is disabled and every call fetches fresh config. The returned Config must not be modified by callers.
If a refresh fails but a previously-cached Config exists, the stale Config is returned so callers continue to operate normally.
form
import "github.com/frob/webform-relay/internal/form"Index
- func FieldsMap(f config.Form) (map[string]config.Field, error)
- func RenderHTML(f config.Form, action string) string
- func RenderHTMLWithHidden(f config.Form, action string, hidden map[string]string) string
- func RenderHTMLWithOptions(f config.Form, action string, opts RenderOptions) string
- func Validate(f config.Form, input submission.Values) error
- func ValidateAll(f config.Form, input submission.Values) []string
- func Visible(field config.Field, values submission.Values) bool
- type FieldError
- type RenderOptions
func FieldsMap
func FieldsMap(f config.Form) (map[string]config.Field, error)FieldsMap returns a map from field name to Field for the given form. It returns an error if any two fields share the same name.
func RenderHTML
func RenderHTML(f config.Form, action string) stringRenderHTML generates an HTML form element for the given form config.
The output is built by hand (rather than via html/template) so that attribute order and whitespace are fully under our control and match the golden fixtures under testdata/golden byte-for-byte.
func RenderHTMLWithHidden
func RenderHTMLWithHidden(f config.Form, action string, hidden map[string]string) stringRenderHTMLWithHidden renders a form like RenderHTML, but with additional server-injected hidden fields (used e.g. to inject a min-fill token). When hidden is nil or empty, the output is byte-identical to RenderHTML(f, action). Otherwise, one <input type=“hidden”> line per key/value pair in hidden is emitted inside the form, immediately before the closing <button type=“submit”> line, with keys in sorted order and both name and value HTML-escaped.
func RenderHTMLWithOptions
func RenderHTMLWithOptions(f config.Form, action string, opts RenderOptions) stringRenderHTMLWithOptions renders a form like RenderHTML, but honoring the supplied RenderOptions: opts.Hidden is injected before the submit button (as in RenderHTMLWithHidden), and opts.Values repopulates any field it covers (per Values.Has), overriding that field’s config default/Selected. A field absent from opts.Values (or a zero-value opts.Values) renders exactly as it would from its config, so RenderHTMLWithOptions(f, action, RenderOptions{}) is byte-identical to RenderHTML(f, action).
func Validate
func Validate(f config.Form, input submission.Values) errorValidate checks that all required fields in f are present and non-blank in input, enforces format rules per field type, rejects values that exceed the maximum allowed length (checked per value for multi-value fields), and enforces each field’s Validate constraints. It returns the first failure message encountered (in field-declaration order), or nil if the submission is valid.
Validate is a thin wrapper around ValidateAll, returning only the first collected message.
func ValidateAll
func ValidateAll(f config.Form, input submission.Values) []stringValidateAll checks all fields/constraints in f against input, in the same field/constraint order Validate uses, and returns every failure message (not just the first) — one message per failing field, in field-declaration order. An empty or nil slice means the submission is valid.
func Visible
func Visible(field config.Field, values submission.Values) boolVisible reports whether field should be shown given the submitted values, per its ShowWhen clause (nil ShowWhen means always visible).
type FieldError
FieldError pairs a failing field’s name with its first failure message.
type FieldError struct {
Field string
Message string
}func ValidateFields
func ValidateFields(f config.Form, input submission.Values) []FieldErrorValidateFields checks all fields/constraints in f against input and returns one FieldError per failing field, in field-declaration order. Field is the field name; Message is that field’s first failure message (the same text ValidateAll produces). An empty slice means the submission is valid, so ValidateFields(f, input)[i].Message equals ValidateAll(f, input)[i].
type RenderOptions
RenderOptions bundles the optional inputs that influence a form render beyond the base config: server-injected hidden inputs and submitted values to repopulate.
type RenderOptions struct {
// Hidden holds server-injected hidden inputs (e.g. a min-fill token),
// emitted before the submit button with keys sorted and escaped.
Hidden map[string]string
// Values, when non-nil AND the field is present (Has), repopulates the
// field with the submitted value(s), overriding config defaults. When nil,
// only config defaults are used (byte-identical to RenderHTML).
Values submission.Values
// Errors maps a field name to an inline error message. When set for a
// field, the renderer emits an inline error element and marks the control
// invalid. When nil/empty, output is byte-identical to today.
Errors map[string]string
// FormEndpoint is the GET /api/v1/form/{form} URL used to htmx-decorate
// controller fields (fields referenced by some field's show_when) so that
// changing them re-fetches and re-renders the whole form. It is DISTINCT
// from the submit action. When "", no field is decorated and output is
// byte-identical to today.
//
// Scaffolding only: declared so tests compile. Renderer wiring (controller
// derivation + hx-* attribute emission) is added by the implementation
// phase; it is currently unused.
FormEndpoint string
}handler
import "github.com/frob/webform-relay/internal/handler"Index
- type CaptchaVerifier
- type ConfigGetter
- type FormRelayer
- type Handler
- func (h *Handler) Handle(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)
- func (h *Handler) IssueToken(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)
- func (h *Handler) RenderForm(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)
- func (h *Handler) Route(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)
type CaptchaVerifier
CaptchaVerifier validates a captcha token against a provider’s API.
type CaptchaVerifier interface {
Verify(ctx context.Context, token string) error
}type ConfigGetter
ConfigGetter retrieves the current config (usually *config.TTLCache).
type ConfigGetter interface {
Get(ctx context.Context) (*config.Config, error)
}type FormRelayer
FormRelayer fans out a validated submission (usually *relay.Relay).
type FormRelayer interface {
Send(ctx context.Context, f config.Form, input submission.Values) error
}type Handler
Handler is the Lambda handler.
type Handler struct {
Config ConfigGetter
Relayer FormRelayer
// Captcha is an optional factory that returns a CaptchaVerifier for the
// given form's captcha config. When nil, captcha verification is skipped.
Captcha func(cfg config.Captcha) CaptchaVerifier
// TokenSigner mints (IssueToken/RenderForm) and verifies (Handle) the
// min-fill hidden-field token for forms with Spam.MinFillSeconds > 0. When a
// form enables min-fill but this is nil or its Secret is empty, those paths
// fail closed (HTTP 500) rather than accepting an unverified submission.
TokenSigner *token.Signer
// Now is the clock used to compute the min-fill token's elapsed time in
// Handle. Nil means time.Now.
Now func() time.Time
}func (*Handler) Handle
func (h *Handler) Handle(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)Handle processes one APIGatewayV2HTTPRequest.
func (*Handler) IssueToken
func (h *Handler) IssueToken(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)IssueToken serves GET /api/v1/token/{form}: it returns the min-fill hidden input for a form that has the min-fill check enabled.
func (*Handler) RenderForm
func (h *Handler) RenderForm(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)RenderForm serves GET /api/v1/form/{form}: it renders the form’s HTML, repopulating fields from the request’s query string, evaluating show_when conditions against those values, injecting the min-fill token hidden field when the form enables the check, and setting FormEndpoint so controller fields keep their htmx re-render decoration.
func (*Handler) Route
func (h *Handler) Route(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)Route dispatches a request to Handle, RenderForm, or IssueToken based on the HTTP method and path.
mapping
import "github.com/frob/webform-relay/internal/mapping"Index
func Apply
func Apply(out config.Output, input submission.Values) submission.ValuesApply builds output values from input using the mapping and static rules in out. For each outputKey → inputKey in out.Mapping, all non-empty values of inputKey are copied under outputKey (multi-value fields are preserved; empty values are skipped). For each outputKey → literalValue in out.Static, the literal is set as a single value (overwriting any mapped value for that key).
relay
import "github.com/frob/webform-relay/internal/relay"Index
type EmailTarget
EmailTarget sends via Amazon SES.
type EmailTarget struct {
Client SESClient
From string
}func (*EmailTarget) Send
func (e *EmailTarget) Send(ctx context.Context, out config.Output, data submission.Values) errorSend delivers data as a plain-text email via SES.
type HTTPTarget
HTTPTarget sends form data as application/x-www-form-urlencoded POST.
type HTTPTarget struct {
Client *http.Client // if nil, use defaultHTTPClient
}func (*HTTPTarget) Send
func (h *HTTPTarget) Send(ctx context.Context, out config.Output, data submission.Values) errorSend posts data to out.URL as application/x-www-form-urlencoded.
type Relay
Relay fans out to all targets registered for a given output type.
type Relay struct {
Targets map[string]Target // keyed by output type string
}func (*Relay) Send
func (r *Relay) Send(ctx context.Context, f config.Form, input submission.Values) errorSend fans out one submission to all outputs on the form. All outputs are attempted even if one fails.
Each output’s on_error policy determines whether its failure surfaces in the returned error. A “continue”-policy output that fails is logged but does not fail the submission. A “fail”-policy output (the default) that fails still contributes to the returned error, exactly as before.
type SESClient
SESClient is the subset of SES API needed.
type SESClient interface {
SendEmail(ctx context.Context, in *sesv2.SendEmailInput, opts ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error)
}type SMTPTarget
SMTPTarget sends form data as a plain-text email via SMTP. Port 465 uses implicit TLS (SMTPS). All other ports attempt STARTTLS if the server advertises it. Auth uses smtp.PlainAuth when Username is set.
type SMTPTarget struct{}func (*SMTPTarget) Send
func (s *SMTPTarget) Send(ctx context.Context, out config.Output, data submission.Values) errorSend delivers the data as a plain-text email body via SMTP.
type Target
Target is implemented by each output type.
type Target interface {
Send(ctx context.Context, out config.Output, data submission.Values) error
}submission
import "github.com/frob/webform-relay/internal/submission"Package submission holds a parsed form submission, preserving multiple values per field so that checkboxes and multi-select survive end to end (S5).
Index
- type Values
- func FromMap(m map[string]string) Values
- func New(v url.Values) Values
- func (s *Values) Add(name, val string)
- func (s Values) All(name string) []string
- func (s *Values) Delete(name string)
- func (s Values) Encode() string
- func (s Values) First(name string) string
- func (s Values) Has(name string) bool
- func (s Values) Len() int
- func (s Values) Map() map[string][]string
- func (s Values) Names() []string
- func (s *Values) Set(name string, vals …string)
type Values
Values wraps a parsed submission. A field may carry more than one value.
type Values struct {
// contains filtered or unexported fields
}func FromMap
func FromMap(m map[string]string) ValuesFromMap builds Values from a single-valued map (one value per key).
func New
func New(v url.Values) ValuesNew wraps parsed URL-encoded values.
New stores a clone of v so that later mutation of the caller’s url.Values does not affect the returned Values. The internal map is always initialized (even when v is nil), so Set/Add never panic.
func (*Values) Add
func (s *Values) Add(name, val string)Add appends val to the values for name.
func (Values) All
func (s Values) All(name string) []stringAll returns every value for name (a copy), or nil if absent.
func (*Values) Delete
func (s *Values) Delete(name string)Delete removes a field.
func (Values) Encode
func (s Values) Encode() stringEncode returns the url.Values encoding (sorted, multi-value preserved).
func (Values) First
func (s Values) First(name string) stringFirst returns the first value for name, or "" if absent.
func (Values) Has
func (s Values) Has(name string) boolHas reports whether name was submitted at all.
func (Values) Len
func (s Values) Len() intLen returns the number of distinct field names.
func (Values) Map
func (s Values) Map() map[string][]stringMap returns a copy of the submission as a plain map of field name to values. Mutating the returned map (or its slices) must not affect the Values.
func (Values) Names
func (s Values) Names() []stringNames returns the sorted, distinct field names.
func (*Values) Set
func (s *Values) Set(name string, vals ...string)Set replaces the values for name.
template
import "github.com/frob/webform-relay/internal/template"Package template renders the restricted, server-side templates used in string positions of a form config (output subjects, static values, headers, and secrets). The data scope and function set are deliberately locked (S6).
Index
func Check
func Check(tmpl string) errorCheck reports whether tmpl is a valid template: it must parse and reference only the locked function set. It returns nil for a valid template and an error for a syntax error or an unknown function.
func Render
func Render(tmpl string, data Data) (string, error)Render renders tmpl against data using the locked scope and function set. Templates are parsed with text/template (not html/template): output positions are subjects, headers, and other plain-text/plain-value fields, not HTML, so no escaping is applied.
type Data
Data is the entire scope available to a template. Nothing outside this is reachable — no request context, no arbitrary environment, no functions beyond the locked set.
type Data struct {
Fields map[string][]string
FormID string
SubmittedAt string
Env map[string]string
}token
import "github.com/frob/webform-relay/internal/token"Package token mints and verifies HMAC-signed, form-bound, timestamped tokens used by the min-fill anti-spam check. A token proves a server issued a timestamp for a specific form; it carries no secret and needs no server state (see docs/content/plans/min-fill-token.md, “Token scheme”).
Index
type Signer
Signer mints and verifies form-bound, timestamped HMAC tokens.
type Signer struct {
Secret []byte // HMAC key
Now func() time.Time // clock seam; nil means time.Now
}func (*Signer) Sign
func (s *Signer) Sign(formID string) stringSign returns a token binding formID to the current time.
func (*Signer) Verify
func (s *Signer) Verify(formID, token string) (issuedAt time.Time, err error)Verify checks the token’s HMAC against formID and returns the embedded issue time. A malformed token, a bad MAC, or a token minted for a different formID returns a non-nil error.
Generated by gomarkdoc