I read this as a read-only security audit of r44’s invite-to-check-in trust boundaries - I’ll trace issuance, transport, claim, activation, persistence, and recovery end to end, then report evidence-backed blockers and exact remediations.


The issuance and claim store are mapped; I’m now tracing the live Telegram ingress, consent callback provenance, registry mutation, and activation cutover to verify that the persisted controls are actually enforced at each runtime boundary.


The end-to-end trace has exposed security-significant gaps around intended-recipient binding and consent provenance, plus a restart dead-end after uncertain Telegram delivery. I’m validating those findings with local temporary-directory tests only; no network, messages, or repository writes.


I read this as the completed r44 security audit - I traced every invite-to-activation boundary and validated the critical findings with disposable local tests.

# Verdict

**FAIL — HIGH severity**
**blocker_count: 4**

No repository files were modified. No network calls or live Telegram messages were made.

## Blockers

### B1 — HIGH: stolen/forwarded invite can be claimed by the wrong account

**Evidence**

- `gateway/platforms/telegram_customer_bootstrap.py:326-329` explicitly rejects drafts containing `customer_user_id`.
- `gateway/platforms/telegram_customer_bootstrap.py:380-411` binds the invite to whichever non-owner Telegram user claims it first.
- The only identity checks are private DM, `user_id == chat_id`, and claimant not being the owner.
- Disposable test confirmed user `666` could claim an invite with no intended recipient binding.

**Impact**

Possession of the bearer link is sufficient to become the registered customer. A forwarded link, compromised operator output, or link sent to the wrong Telegram account permanently registers the wrong identity and consumes the token.

**Exact remediation**

1. Require the intended Telegram `customer_user_id` when issuing the invite.
2. Persist its digest or exact numeric ID in the session.
3. During claim, atomically require `from_user.id == intended_customer_user_id`.
4. If recipient identity cannot be known beforehand, add a second owner-authenticated approval step that displays the claimed Telegram identity and blocks registration, consent, and activation until approved.
5. Keep the generic rejection response to avoid identity disclosure.

---

### B2 — HIGH: activation does not require durable, accurate consent evidence

**Evidence**

- `gateway/platforms/telegram.py:6743` records registry consent before durable bootstrap handoff evidence exists.
- `gateway/platforms/telegram.py:6759-6778` advances to `AWAITING_ACTIVATION`; starting the evidence runtime is optional.
- `gateway/platforms/dualcoach_activation_cutover.py:193-224` accepts `AWAITING_ACTIVATION` without requiring `session.consent_handoff`, its provenance digest, or reconciliation receipt.
- `gateway/platforms/telegram_nutrition_onboarding_runtime.py:360-409` uses `message.date` as `recorded_at`. For callback queries this is the consent card’s publication time, not the user’s click time.
- Disposable test confirmed the persisted consent timestamp equals the old card timestamp.

**Impact**

The activation authority can be satisfied with registry consent but no durable proof of the exact authenticated callback. When evidence is present, it falsely dates consent to card publication, potentially before the customer acted.

**Exact remediation**

1. Persist the authenticated callback handoff before mutating canonical consent or advancing bootstrap state.
2. Capture a trusted callback-ingress timestamp from the polling/webhook receipt, not `query.message.date`.
3. Bind the receipt to session ID, generation, customer ID, actor/chat/topic, card message ID, callback data, update ID, and ingress timestamp.
4. Make activation require:
   - one valid `consent_handoff`,
   - current notice version,
   - matching registry consent,
   - matching provenance digest,
   - successful business-commit reconciliation.
5. Perform the consent transition under one durable CAS/outbox protocol so crashes cannot expose `AWAITING_ACTIVATION` without evidence.

---

### B3 — MEDIUM: bootstrap receipts are mutable snapshots, not append-only

**Evidence**

- `gateway/platforms/telegram_customer_bootstrap.py:306-307` uses one `ledger.json`.
- `gateway/platforms/telegram_customer_bootstrap.py:877-932` reads a self-digested snapshot.
- `gateway/platforms/telegram_customer_bootstrap.py:934-970` rewrites the entire ledger with `os.replace`.
- Issuance, claim, consent publication, consent handoff, and activation do not have an append-only transition receipt chain.

**Impact**

Prior states and transitions can be removed or rewritten without detectable history; the unkeyed digest can simply be recomputed. This prevents reliable forensic proof of token issuance, single-use claim, consent, or activation ordering.

**Exact remediation**

Replace the snapshot authority with, or back it by, a mode-0600 append-only journal:

- One hash-chained row per transition.
- Include prior digest, session ID, generation, event type, timestamp, actor/route digests, and payload digest.
- Write with `O_APPEND | O_NOFOLLOW`, lock, `fsync(file)`, then `fsync(directory)`.
- Reconstruct current state from the verified chain at startup.
- Reject truncation, duplicate generations, invalid transitions, and conflicting terminal events.
- Keep raw invite tokens out of the journal.

---

### B4 — MEDIUM: uncertain consent-card delivery is permanently unrecoverable

**Evidence**

- `gateway/platforms/telegram.py:12128-12172` persists a recovery attempt before sending.
- A timeout/network uncertainty intentionally leaves that attempt unresolved.
- `gateway/platforms/telegram.py:5794-5798` skips every restart recovery session with `recovery_attempts`.
- `gateway/platforms/telegram_customer_bootstrap.py:593-641` provides reservation and immediate receipt binding, but no restart reconciliation of an uncertain provider result.
- Disposable restart test retained `AWAITING_CONSENT`, one unresolved attempt, and no receipt.

**Impact**

A crash or uncertain Telegram response after successful delivery leaves the customer permanently blocked. The flow fails closed, but cannot recover without manual state surgery.

**Exact remediation**

Use a durable publication outbox:

1. Assign a stable dispatch identity before sending.
2. Persist `reserved`, then send with that identity.
3. Persist the provider receipt atomically when available.
4. On restart, reconcile the exact dispatch through a durable provider/polling receipt.
5. If absence is proven, release and retry once with the same dispatch identity.
6. If outcome remains unknowable, expose an owner-authorized resolution command that records an append-only decision; never silently resend.

## Boundary results

| Boundary | Result | Evidence |
|---|---|---|
| 24-hour TTL | PASS | Exact boundary expiration in `telegram_customer_bootstrap.py:365-377, 850-867`; tested. |
| Atomic single use | PASS | Generation-CAS under `flock` at `:380-415, 815-844`; replay test passed. |
| Token secrecy at rest | PASS | 128-bit URL-safe token; only SHA-256 stored at `:330-360`. Raw link is intentionally returned once. |
| Wrong-account claim | **FAIL** | B1. |
| DM-only enforcement | PASS | Telegram private chat and `user_id == chat_id` at `telegram.py:11998-12017`; repeated in store and registration. |
| Callback/card binding | PASS | Exact customer route and current consent-card message ID at `telegram.py:6708-6740`; stale cards rejected. |
| Consent evidence | **FAIL** | B2. |
| Owner authority | PASS | Registry owner is checked during registration and runtime authority resolution; activation uses external Task26 authority. |
| Membership evidence | PASS | Fresh Bot API absence proof, staff inventory binding, five-minute freshness, and deployment binding in `telegram_staff_membership_gate.py:318-367, 508-674` and activation cutover `:284-358`. |
| Activation race/replay | PASS | Profile authority lock, committed activation validation, generation CAS, and idempotent ACTIVE reconciliation at `dualcoach_activation_cutover.py:145-224` and bootstrap `:491-557`. |
| Capacity | PASS | Issuance precheck at `dualcoach_customer_invite.py:120-133`; activation rechecks under profile lock in `customer_admin.py:1038-1055`. |
| Symlink/permissions/traversal | PASS | Absolute owner-only profile, 0700 directories, 0600 single-link files, no-follow lock handling, and constrained customer-key model. |
| Cross-customer isolation | PASS | Unique registry keys/routes/spaces and ambiguous chat/session lookups fail closed. |
| PII leakage | PASS | Raw token is absent from storage/logs; Telegram errors are generic; state files are private. |
| Append-only receipts | **FAIL** | B3. Membership journal is append-only, bootstrap lifecycle is not. |
| Restart recovery | **FAIL** | B4. Normal registration/activation recovery exists; uncertain send recovery does not. |
| Fail-closed behavior | **FAIL** | Most ingress/storage failures close safely, but activation’s missing consent-evidence prerequisite violates fail-closed authorization. |

## Verification

- `pytest -q tests/gateway/test_dualcoach_admin_customer_invite.py tests/gateway/test_telegram_customer_bootstrap_invite_ttl.py`
- Result: **7 passed**
- Disposable tests reproduced:
  - arbitrary first-user invite claim,
  - rejection of pre-bound recipient drafts,
  - unresolved publication surviving restart,
  - consent evidence using card publication time.