Skip to content

[i,l,-r] Slack approval follow-ups: single-use JWTs, approver DM update, picker copy (SECOP-1093/1094/1095)#13

Merged
phosphore merged 4 commits into
masterfrom
lorenzo/secop-1093-1095-slack-followups
Jul 9, 2026
Merged

[i,l,-r] Slack approval follow-ups: single-use JWTs, approver DM update, picker copy (SECOP-1093/1094/1095)#13
phosphore merged 4 commits into
masterfrom
lorenzo/secop-1093-1095-slack-followups

Conversation

@phosphore

Copy link
Copy Markdown
Member

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 JWT jti; ProposalHandler gains verifyNotConsumed/markConsumed hooks (default no-op; Slack handler enforces via the existing Firestore registry — HMAC-keyed markers in the same collection, reaped by the existing expires_at TTL). ProposalResource checks 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.9 version 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

phosphore added 4 commits July 9, 2026 18:06
…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.
@phosphore phosphore merged commit 8148b93 into master Jul 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant