Raxx · internal docs

internal · gated

DET-BETA-006 — join token sharing

Rule ID: DET-BETA-006 Title: Beta join token claimed from IP inconsistent with prior state-check history — token sharing Category: beta Last validated: 2026-06-18 (beta-phase2 catalog; grounded against as-built beta_join.py) State: liveFLAG_BETA_PHASE2_ACCESS is ON in prod; POST /api/beta/join/<token>/claim emits beta.join.claimed audit events to audit_log. Claim events are queryable from Raptor Postgres directly (no drain dependency).

Why this detection exists

A beta invite token is minted for a specific tester email and is single-use. If the tester shares their invite link with a third party, the third party can open the state-check page, proceed through the NDA flow, and then claim the token via POST /api/beta/join/<token>/claim. The account is created with the tester's email address (the one embedded in the token), so the tester cannot then use their own invite — they are left without an account.

The detection is informational: the account created is legitimate (tester's email, valid single-use token). The structural harm is that the tester loses access to their own invite. The detection surfaces the pattern so the operator can issue a replacement token if warranted.

Two signals are observable from the as-built claim endpoint (backend_v2/api/routes/beta_join.py):

  1. IP mismatch between state-check and claim. The join_claim() handler writes beta.join.claimed to audit_log with ip_prefix (computed as /24 for IPv4, /48 for IPv6, via _ip_prefix()). If prior state-check requests for the same jti were observed from a different IP /24 (available once the Heroku drain is wired), the claim IP mismatch is the signal.

  2. Rapid re-claim attempt after consumption. If the original tester then tries to use their own link after the third party consumed it, call_consume(token, check_only=False) returns status == "already_consumed". The join_claim() handler logs this as beta_join.claim already_consumed email_hash=... jti=... at WARNING level and returns 409 {"error": "already_claimed"}. A 409 on a jti that was successfully claimed from a different IP /24 within 60 minutes is the sharing-confirmation signal.

Telemetry source

Telemetry availability: audit_log is Raptor Postgres, queryable directly (no Heroku drain dependency). This detection is operationally stronger than DET-BETA-005 and DET-BETA-007 for the 409-based Rule 2 signal.

Statistical method + baseline window

This is an event-pair detection, not a statistical threshold. The signal is structural:

Pre-baseline: no rolling window needed — these are point-in-time event pairs with a 60-minute observation window.

Threshold + expected FP rate

Rule 2 (rapid 409 from different IP) is the higher-confidence signal. Rule 1 alone is informational, not actionable.

Alert route

Escalation owner

Test fixture / synthetic positive

See _fixtures/join_token_sharing_positive.json — synthetic pair: (1) beta.join.claimed event for jti=synth-jti-001 from IP prefix 192.0.2.0/24, (2) beta_join.claim already_consumed log for same jti from IP prefix 198.51.100.0/24 43 minutes later, representing the original tester attempting to use their own link after a third party claimed it.

Manual query (Postgres direct — no drain required for Rule 2)

-- Find claimed events; cross-reference logs for rapid re-claim attempts on the same jti
SELECT target_id AS jti,
       context->>'tester_email_hash' AS email_hash,
       ip_prefix,
       created_at
FROM audit_log
WHERE action = 'beta.join.claimed'
ORDER BY created_at DESC
LIMIT 100;

Cross-reference returned jti values against Raptor logs for beta_join.claim already_consumed entries on the same jti within 60 minutes:

heroku logs --app raxx-api-prod --num 5000 \
  | grep 'beta_join.claim already_consumed'

What NOT to do