#787 registered at 18:35 CT, 22 minutes after the first join-flow fix, and still had no profile. Two
causes, both fixed:
1. messages.verifyChallenge resolved the wallet through chain.memberIdByAccount, which reads the
CACHED index. Seconds after a registration that wallet is not in it, so the signature was rejected
with "No RM Circle position is registered to this wallet". It now accepts an idHint (the position
id from the member's own registration receipt) and, on a cache miss, reads that id live from the
contract via chain.verifyMember, minting only when the contract says this exact wallet owns it.
That is a stronger proof than the cache, not a weaker one. Now async; the single call site awaits.
2. join-now.js fired the sign-in and a 4.5s redirect in parallel, so the page could navigate away
while the wallet was still showing the signature prompt, and it did not wait for submit-id (which
runs the live verifyMember server-side that seeds the index). It now awaits the report, passes the
receipt id, and redirects only once the signature settles, with a 120s bailout.
qa/signin-fallback.mjs (7 assertions) proves the cold-index path with real secp256k1 signatures and
covers the abuse cases: a hint for a position the wallet does not own is refused, and a signature from
another wallet is refused. Existing suites still pass: profiles-unit 28, gate-e2e 47.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A member holding #2 and #3 connected #3's wallet, closed the browser, came
back, and still saw #2. Not a wallet problem: the sign-in cookie lasts 30 days
and there was no sign-out anywhere on the site, so the first position he
authenticated as was pinned to that browser and the wallet was never consulted
again.
This hits precisely the people we tell to buy several positions - the Triple
Play is on /how-pay-works and in the chatbot - so it will keep happening.
Adds POST /api/public/signout (drops the server session and expires the
cookie) and a "not this position? switch" link beside the identity line on
/suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two sign-in failures with different causes and no way to tell them apart from
the outside, so stop guessing and get the data off the member's device.
/wallet-check reports what the page can actually see: whether window.ethereum
exists, which wallet flags are set, which EIP-6963 wallets announced, the
accounts and chain the wallet returns — and critically, any CSP violation.
Our script-src is 'self' with no 'unsafe-inline', and a wallet browser that
injects its provider via a script tag would be blocked silently, producing
symptoms identical to "no wallet installed". The company dApp sends no CSP at
all, which is a plausible reason it connects where we do not. This page will
confirm or kill that theory rather than us theorising further.
"Signature does not match this wallet" was true but useless — it never said
WHICH wallet signed. It now names both the address the page asked for and the
address that actually signed, which identifies the classic multi-account case
(wallet signs with the selected account, not the one the page picked up) in
one glance.
Verified for the case in hand: 0x20E0…2248 IS the wallet registered to
position #52 on-chain, so ownership was never the problem.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /api/public/tg-webapp-auth: HMAC-verifies WebApp initData against the
companion bot token (12h freshness, timing-safe), maps chat -> member via
tg-links.json, mints a message session -> linked members land on /my/<id>
with zero login
- /app entry page (vendored telegram-web-app.js keeps CSP script-src 'self');
unlinked users get the one-time wallet-link instructions
- tg-app.js on all pages: no-op in browsers; inside the webview lazy-loads the
SDK, expands, themes header/background #071421, wires native BackButton
- Bot menu button set programmatically to open /app; /start + help mention it
- Synced chat.js canned answer + AI system prompt (Mini App facts)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- messages.js: challenge/personal_sign/recover auth (vendored pinned
js-sha3 0.9.3 + noble-secp256k1 1.7.1, server-side only; self-tested
positive + tamper cases), 30d HttpOnly sessions, message store on the
volume, matrix-line permissions (your downline direct or broadcast, your
upline chain - nothing else, so spam is impossible by construction),
daily rate limits (30 direct / 3 broadcasts), 1500-char plain text
- chain.js: memberIdByAccount (wallet -> position for sign-in)
- API: msg-challenge/-verify/-me/-inbox/-send/-read public + msg-unread
(count only, no auth) + admin/messages (full visibility, disclosed to
members in the UI)
- Dashboard: Messages card with unread bell, one-tap wallet sign-in,
inbox with auto-read, compose with to-ID or whole-team broadcast
- Admin: Member Messages review table
- Chatbot canned answer + AI system prompt updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>