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) string

TokenField 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) Verifier

New returns a Verifier that validates tokens against the provider’s public endpoint.

func NewWithEndpoint

func NewWithEndpoint(cfg config.Captcha, client *http.Client, endpoint string) Verifier

NewWithEndpoint 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) int

Run 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

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() bool

RequiresTokenSecret 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

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) string

RenderHTML 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) string

RenderHTMLWithHidden 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) string

RenderHTMLWithOptions 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) error

Validate 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) []string

ValidateAll 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) bool

Visible 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) []FieldError

ValidateFields 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

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.Values

Apply 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) error

Send 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) error

Send 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) error

Send 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) error

Send 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

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) Values

FromMap builds Values from a single-valued map (one value per key).

func New

func New(v url.Values) Values

New 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) []string

All 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() string

Encode returns the url.Values encoding (sorted, multi-value preserved).

func (Values) First

func (s Values) First(name string) string

First returns the first value for name, or "" if absent.

func (Values) Has

func (s Values) Has(name string) bool

Has reports whether name was submitted at all.

func (Values) Len

func (s Values) Len() int

Len returns the number of distinct field names.

func (Values) Map

func (s Values) Map() map[string][]string

Map 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() []string

Names 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) error

Check 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) string

Sign 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