Skip to content

feat: roster signals — origin, GitHub reputation, Slack SSO, bounces - #189

Merged
themightychris merged 8 commits into
developfrom
feat/roster-signals
Sep 18, 2026
Merged

themightychris merged 8 commits into
developfrom
feat/roster-signals

Conversation

@themightychris

Copy link
Copy Markdown
Member

Implements plans/roster-signals.md. Follow-up to #187 from the first day on /admin/members: the roster now says what a careful reader would conclude, and it carries the signals that matter most for spam arriving through GitHub.

Roster

  • Badges: Signed up here / Imported (origin); GitHub · @login · age · repos · followers linking to the profile (red "GitHub account gone" when GitHub 404s the account; amber when < 30 days old or zero activity); Slack · {when}; Email bounced · {type}; email ≠ name; {n} links in bio; no bio; no avatar.
  • Attention tint from row-local signals (amber ≥ 3, red ≥ 5 or on gone/bounced). Never consults machine verdicts.
  • Default filter is "no vote yet"; new origin filter; sign-in count; j/k/s/n/Enter keyboard triage.

Data behind it (all on the private profile)

  • GitHub facts captured at every GitHub sign-in (created_at, public_repos, followers, following, type) — we were already fetching them and throwing them away.
  • GitHub probe: GET /user/{id} with the OAuth app's client credentials for linked rows on the page whose record is older than a day (bounded concurrency; a failed probe keeps the old record). status = gone ⇒ GitHub deleted or suspended the account.
  • lastSlackSsoAt stamped on every assertion the IdP issues.
  • Postmark bounce webhook POST /api/_webhooks/postmark/bounce (POSTMARK_WEBHOOK_SECRET, bearer or basic-auth) records terminal bounce types — no SMTP probing.

Pipeline

probe-github evaluator in the private repo (codeforphilly-spam-detection): 404 → spam 0.95; account > 1y with repos/followers → legit 0.9 (protects from every machine verdict); else uncertain.

Validation

type-check + lint clean; API 39 files / 473 tests (new: signals units, probe gating/persistence, webhook auth + mapping, SSO stamp); web 28 / 124.

To turn on in prod

  • Seal POSTMARK_WEBHOOK_SECRET into codeforphilly-secrets and add the webhook URL in Postmark with basic auth.
  • Add GITHUB_OAUTH_CLIENT_ID/SECRET to the private repo's .env for probe-github. The roster probe uses the same OAuth app credentials already sealed for sign-in.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ

themightychris and others added 8 commits September 18, 2026 16:17
…ce webhook

The roster gains origin, GitHub facts (captured at sign-in, re-probed
daily), Slack SSO recency, Postmark bounces, and row-local signals with
an attention tint; the private profile grows the three fields behind
them; a new webhook spec covers Postmark bounces; the pipeline gains a
github-probe evaluator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
…t probe

/user already returns created_at, public_repos, followers, following
and type; store them on the private profile on every GitHub sign-in.
probeGitHubUser() asks GET /user/{id} with the OAuth app's client
credentials — a 404 means GitHub deleted or suspended the account.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
Best-effort private-profile write on all three assertion paths so the
roster can show whether a member has actually signed into Slack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
POST /api/_webhooks/postmark/bounce, guarded by POSTMARK_WEBHOOK_SECRET
(bearer or basic-auth password), stores terminal bounce types on the
member's private profile. A hard bounce on mail we already send is the
reliable "mailbox disabled" signal; no SMTP probing of our own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
…, signals

Row-local signals (never-signed-in, no-avatar, no-bio, bio links,
email/name mismatch, bounced email, GitHub gone/new/inactive) count
toward an attention score; positives (slack-sso, has-footprint) are
informational. Linked accounts on the requested page are re-probed
against GitHub when their record is older than a day, with bounded
concurrency and a failure-keeps-old-record rule. New origin filter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
Signed-up-here / Imported, GitHub (linked, with age, repos, followers;
red when gone; amber when new or inactive), Slack SSO recency, bounced
email, email-vs-name mismatch, links in bio, no bio / no avatar. Rows
tint amber at three negative signals and red at five or on a critical
one. Default filter is "no vote yet"; j/k/s/n/Enter drive triage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFyA5poHwrhAktrnsKrUiQ
@themightychris
themightychris merged commit 4ea4f30 into develop Sep 18, 2026
1 of 2 checks passed
@themightychris
themightychris deleted the feat/roster-signals branch September 18, 2026 20:26
@themightychris themightychris mentioned this pull request Sep 18, 2026
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