[i,l,-r] Slack approval follow-ups: single-use JWTs, approver DM update, picker copy (SECOP-1093/1094/1095)#13
Merged
Conversation
…095) Since SECOP-952 (#11) an empty selection no longer notifies every qualified peer on broad ACLs — it narrows to the requester's closest teammates (small group-free sets still notify everyone). Update the three places in the picker UI that still described the old behaviour: the hint text, the default-unchecked comment, and the degraded-picker notice.
The sibling-update loop skipped the approver, leaving their DM frozen with a stale action link. In the picked-single-reviewer flow (sole recipient) nothing changed on screen at all, which read as a failed approval (live-test feedback, 2026-07-09). Every recorded DM is now updated in place: siblings get the existing 'already approved by X' blocks, the approver gets a new 'you approved this request' variant.
Proposal tokens are stateless signed JWTs (~1h validity) with no consumption record, so one approval link authorized unlimited re-grants for the token lifetime, each resetting the membership clock — observed live during the SECOP-952 validation (an approver double-clicked and re-granted a lapsed 15-minute membership). - Proposal.id() surfaces the JWT jti (crypto-random, minted at propose). - ProposalHandler gains verifyNotConsumed/markConsumed hooks; defaults are no-ops so the mail/debug handlers keep upstream semantics. - SlackProposalHandler enforces via SlackMessageRegistry: consumption markers live in the same Firestore collection as DM registry entries (HMAC-keyed under the existing salt, namespaced with a "consumed|" prefix, reaped by the existing expires_at TTL policy at token expiry). - ProposalResource checks consumption on GET (used link renders the "URL no longer valid" banner) and POST (before the approval executes), and records it right after a successful approval. - Failure mode is deliberately fail-open with ERROR logs: the JWT signature remains the authoritative authorization; consumption is anti-replay defense-in-depth and approvals must not hard-depend on Firestore availability.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three follow-ups from the SECOP-952 live validation (2026-07-09), one commit each:
SECOP-1093 — single-use approval JWTs (security)
Approval links were replayable for the full ~1h token lifetime; each re-click re-granted a fresh membership window (observed live: a double-click re-elevated a lapsed 15-minute grant). Now:
Proposal.id()surfaces the JWTjti;ProposalHandlergainsverifyNotConsumed/markConsumedhooks (default no-op; Slack handler enforces via the existing Firestore registry — HMAC-keyed markers in the same collection, reaped by the existingexpires_atTTL).ProposalResourcechecks on GET (used link → "URL no longer valid" banner) and POST (before the approval executes), records after success. Fail-open on Firestore errors with ERROR logs — the JWT stays authoritative; replay protection degrades rather than blocking approvals.SECOP-1094 — approver's own DM updates after approval (UX)
The sibling-update loop skipped the approver, leaving their DM frozen with a stale action link — in the picked-single-reviewer flow nothing changed at all ("I'm not sure it really works though?"). Every recorded DM now updates in place: siblings get "already approved by X", the approver gets a new "✅ You approved this request" variant.
SECOP-1095 — picker helper copy (UX)
Three spots still described the pre-auto-narrow behavior ("leave all unchecked to notify every qualified peer"); updated to match SECOP-952 semantics.
Plus the
2.3.0-wavemm.9version bump for the next release.Tests: 13 new (consumption enforcement + fail-open, approver-DM variants incl. sole-recipient, jti round-trip, consumption-key properties); full suite 1030/1030 locally.
Deploys with the next wavemm-iam submodule bump — no infra changes needed (reuses the existing Firestore collection and TTL policy).
🤖 Generated with Claude Code