From 2e1ed6e0d8fa67c7fead974821ba16e14fb304c3 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 21:17:04 +0200 Subject: [PATCH] docs(poll): document governed participation boundary --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 4c225ac..a5ab86d 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,72 @@ and authenticated API routes for: - signed participation links for reduced/no-Access participation - context and workflow metadata for modules such as Scheduling +## Governed signed participation + +The v0.1.10 contract lets a consuming module bind an invitation to an exact +response gateway (`module_id`, `resource_type`, and `resource_id`) and snapshot +generic participation rules onto it. A bound invitation cannot use the legacy +`GET /poll/public/{token}` or `POST /poll/public/{token}/responses` routes: both +return the same generic not-found response used for invalid, expired, and +revoked links. There is deliberately no browser-facing Poll bypass when the +owning gateway is missing. + +That boundary is Poll-wide. Setting `context_module` declares a module-owned +Poll; for a standalone Poll, its first governed invitation opts the whole Poll +into governed participation. From then on, direct authenticated response +writes, ordinary invitation creation, and every legacy public link fail closed, +including links created before governance was enabled. Governed invitations can +only be created through the in-process participation capability and their +gateway must match the Poll's declared module resource (or the standalone +Poll's first gateway). Polls that never opt into governance retain their +ordinary authenticated and signed-link behavior. + +Module ownership also protects management boundaries. Generic Poll update, +lifecycle, option, and invitation-revocation paths cannot mutate an owned Poll, +and generic raw-response and invitation projections cannot bypass the owning +module's privacy policy. The in-process capability supplies the exact module, +resource type, and resource id again for every mutation; Poll compares that +owner while holding its row lock. Aggregate result summaries remain reusable. + +Consumers resolve and submit bound invitations through the +`poll.participation_gateway` in-process capability. The policy covers: + +- at most one non-`unavailable` selection +- whether `maybe` is accepted +- a maximum number of definitive participants per option +- response comments (trimmed and limited to 4,000 characters) +- email for anonymous participants +- an anonymous-password verification requirement + +For availability polls, `available` reserves capacity while `maybe` does not. +Poll re-enforces these rules under its Poll-row lock, so concurrent final-place +claims serialize on PostgreSQL. A gateway-owned password never crosses the +capability boundary: the owning module stores the sole salted verifier, +throttles attempts before checking it, and supplies only the +`anonymous_password` verification attestation. Poll rejects secret-like +invitation/response metadata and stores no password column. + +Authenticated module flows do not need to retain a public bearer token. They +can resolve and submit by exact tenant, Poll, governed invitation, gateway, and +respondent identifiers. Poll rejects mismatched identities and applies the same +Poll-row lock and policy checks used by the public-token path. A consumer can +lazily create that governed invitation, retain only its id, and discard the raw +token when no public link is needed. + +An owning gateway can update a non-revoked invitation's expiry in place. A past +timestamp expires the existing link immediately; a later future timestamp or +`null` reactivates that same link without exposing or rotating its bearer token. +Revoked invitations remain revoked, and exact retries are idempotent. + +The capability also supports durable invitation-scoped idempotency keys, +response prefill, option addition/removal, and invitation revocation. Exact +submission retries return the existing response; key reuse with different +content is rejected. Because a response is an editable resource, replay returns +its current representation rather than an immutable snapshot of the first +submission. Option removal and option changes invalidate only answers bound to +that option, and repeated removal/revocation is an idempotent replay even after +the Poll has moved out of an editable lifecycle state. + ## Scheduling As A Poll-Backed Workflow Scheduling should use Poll as the reusable response collection primitive, not