← All stories

BRANCH · ef-014-invitation-reveal

Invitation Reveal

EF-014 Persona: Public guest (no auth) Stage: Public Roots in: public-event-page

An invited guest reaches a public event page without their invitation link. They can enter their email to reveal the personal invite path, but the page must not become an invitation-list oracle. Matching and non-matching emails use the same calm response shape unless a verified invitation can be sent or displayed to that exact email.

Preconditions

This branch inherits the public event page trunk after the event page resolves. It does not retest trunk-level not-found, archived, draft, capacity-at-page-load, invite-only-without-token, or base Open Graph behavior except where the invitation-reveal flow changes the outcome.

Happy path

  1. Guest opens the public event page and chooses “Find my invitation.”

    The reveal panel is an inline form using ui-form and ui-text-input. It asks only for email and keeps event identity visible.

  2. Guest submits the email address used by the organizer.

    The server performs a normalized, case-insensitive lookup scoped to the event and sends or reveals only that guest's invitation link.

  3. Invite found.

    The page shows a neutral success state: “Check your email for your personal invitation link.” If policy allows on-page reveal, the CTA opens the tokenized RSVP path for the matched guest.

Failure modes

Capacity full mid-fill

Trigger: the guest starts lookup while seats remain; another guest takes the last seat before submit.

Lookup still completes, but the invite-link result tells the guest the event just filled and presents the waitlist path. The 409 is not rendered as a broken lookup.

Two-tab idempotency

Trigger: the same guest submits the same email in two tabs.

Both requests share the lookup idempotency semantics; only one reveal audit row and one resend notification are created. The second tab shows the existing reveal outcome.

Network drop during submit

Trigger: the lookup POST lands but the response is lost.

Retry sends the same Idempotency-Key. The guest sees one outcome and receives at most one invitation email for the retry window.

Invalid input rejected without info leak

Trigger: a guessed event slug or fake token is submitted alongside an email.

Known-not-found, malformed, and unknown-token cases return the same generic 4xx envelope, same timing class, and no token-format diagnostics.

Source page archived, draft, or deleted

Trigger: the reveal form is submitted from an old tab after the event is archived, drafted, or deleted.

The flow-specific response is one generic unavailable state. It does not say whether an invitation existed or whether the event was archived versus deleted.

Browser back after success

Trigger: after reveal success, the guest presses browser back.

The email form is restored read-only with the prior result. It does not resubmit or send another invitation email.

Open Graph tags on reveal URL

Trigger: the guest shares the page URL with reveal query state present.

The rendered HTML still has canonical event og:title, og:description, og:image, and og:url; no email or token state appears in the preview.

Bot-fill rate-limited

Trigger: one IP submits ten lookup emails in thirty seconds.

The form shows a friendly retry-after or captcha challenge. The copy does not confirm whether any submitted address was invited.

Email mismatch anti-probing

Trigger: a visitor enters an address that is not invited.

The response shape matches a privacy-preserving success: “If an invitation exists for that email, we’ll send it.” No “not on list” wording, no count of remaining attempts.

Per-email rate limit

Trigger: an attacker rotates IPs while probing one target email.

The server throttles by normalized email + event. UI gives a generic wait message that is identical for invited and uninvited addresses.

Plus-address normalization

Trigger: guest tries [email protected] when the invite was sent to [email protected].

Normalization follows the workspace email policy. If aliases are not considered equivalent, the response remains generic and does not reveal the canonical invited address.

Stable test attributes

Visibility teeth. Each attribute must be present and effectively visible when the relevant reveal state is active.

data-testWherePurpose
invite-reveal-panelPublic event pageRoot invitation reveal flow
invite-reveal-formInside reveal panelEmail lookup form
invite-reveal-emailInside reveal formEmail input
invite-reveal-submitInside reveal formSubmit lookup
invite-reveal-resultAfter submitGeneric lookup outcome
invite-reveal-link-ctaMatched-invite resultOpen personal invite when policy permits
invite-reveal-rate-limitRate-limited stateRetry-after or captcha message
invite-reveal-unavailableArchived/draft/deleted flow stateGeneric unavailable result
invite-reveal-waitlist-offerCapacity race stateWaitlist offer after reveal submit

Agent test plan

Run public-event-page trunk with page-resolves, then exercise the reveal form against matched, unmatched, throttled, unavailable, and race fixtures. All probes use data-test selectors and allowed predicate vocabulary only.