Pete holds the chips; we hold the euros and are the only one who can move them. This is the loop that turns an escrow row on games.parodia.dev into a real balance change here. It is a poll because Pete cannot call us and isn't going to be able to. Every step is keyed on the escrow guid, because every step can be interrupted in the worst possible place: die after DebitIdem and before the verdict is queued, and Pete re-offers the row, we claim it again, the debit replays as a no-op and reports the same answer. The player is charged once. That is the only property here that really matters, and there is a test that kills us three times to prove it. The verdict rides the queue that already carries adventure facts — it wants the same durability, backoff and parking, so the row now says where it's going rather than the queue growing a twin. Flush sends it at once instead of after a 15s sender tick, because there's a person at the other end watching a spinner. First GET gogobee has ever made to Pete.
5.8 KiB
GogoBee → Appservice Auth Migration (multi-session)
Goal: move TwinBee's Matrix auth from the MAS OAuth 2.0 device grant (working
stopgap) to the full appservice transaction/push model — the MAS-durable end
state: no human login, no MFA, no consent, as_token never expires, scales at
485+ rooms. The device grant stays behind AUTH_MODE=masdevice as instant
rollback.
Why the previous appservice attempt failed: it was a hybrid (as_token + /sync),
and Synapse forbids appservice-namespace users from /sync. This migration
replaces /sync with Synapse→bot transaction push + MSC3202/MSC2409 E2EE
extensions, which is the supported path in mautrix v0.28.1.
Keep this doc working-tree only — do NOT commit (see feedback_dont_commit_plan_mds).
Key facts established (investigation, 2026-07-03)
- HEAD
20d1f92is MISLABELED: commit msg says "device grant" but it only deleted registration.yaml.example; committed client.go is still the FAILED appservice+/sync hybrid. The real device-grant (masauth.go + client.go rewrite) is UNCOMMITTED working tree — that's what runs in prod. Backed up to scratchpad. - mautrix v0.28.1 fully supports appservice E2EE: transaction carries
to_device/device_lists/device_one_time_keys_count(stable + msc2409/msc3202 unstable field names);CreateDeviceMSC4190; cryptohelperMSC4190=truemints the device (no /login). HEAD's old appservice code already had auth+crypto correct — only the /sync event loop was wrong. - To-device processing gates on
Registration.EphemeralEvents(yamlreceive_ephemeral: true) —appservice/http.go:133. - Encrypted rooms: must feed
m.room.encryption/m.room.memberto the client's StateStore viamautrix.UpdateStateStore(...)+mach.HandleMemberEvent, or the bot sends plaintext into encrypted rooms / can't share keys. - Both hosts SSH-reachable:
reala@parodia.dev(Synapse/MAS/Authentik docker),reala@192.168.1.212(millenia, bot in screen). Retired registration at/home/reala/matrix/compose/synapse/gogobee-registration.yaml.retired-20260703-223425.
Decisions (user, 2026-07-03)
- Cutover: AUTH_MODE flag (keep device grant as rollback), not full replace.
- Synapse changes: I apply + restart, confirm right before restart.
Session 1 — bot-side code (local, no prod risk) ✅ DONE (build+vet green)
Config: AuthMode + appservice fields (RegistrationPath, ListenHost/Port, HomeserverDomain).internal/bot/session.go:Sessionunifying both modes (OnEventType/Run/ Stop);NewSession(cfg)dispatches on AuthMode; masdevice path wraps existing device-grant NewClient + DefaultSyncer + sync loop (verbatim).internal/bot/appservice.go: LoadRegistration → CreateFull → BotClient; cryptohelper MSC4190 device create; ep.OnOTK/OnDeviceList + 9 crypto to-device types → crypto machine; EventEncrypted decrypt+redispatch; StateStore population (encryption + members) via lazy per-room resolveRoom + UpdateStateStore on member/encryption state; HTTP listener via as.Start() in goroutine.main.go: NewSession + sess.OnEventType + sess.Run; env AUTH_MODE, AS_REGISTRATION, AS_LISTEN_HOST, AS_LISTEN_PORT, HOMESERVER_DOMAIN; envInt helper..env.exampledocumented; go.mod tidied (zerolog now direct dep).go build/go vetgreen (CGO=1 -tags goolm).
New files: internal/bot/session.go, internal/bot/appservice.go. NOT yet committed.
Known correctness risks to verify in Session 3
- resolveRoom is lazy on inbound encrypted events only. Scheduled/broadcast
posts into an encrypted room the bot hasn't received from yet would send
plaintext (StateStore unresolved). Mitigation for S3: prewarm the configured
rooms (BROADCAST_ROOMS, GAMES_ROOM, ESTEEMED_ROOM, MOD_ADMIN_ROOM, MINIFLUX_*)
at startup, or resolve all joined rooms once. Interactive
!commands are reply-driven so lazy resolve covers them. - Whether crypto machine successfully shares keys after ReplaceCachedMembers + device fetch (FetchKeys) — the make-or-break, only testable against live Synapse.
- MSC4190 device creation must succeed against MAS-fronted Synapse (untested end to end; HEAD's old code used the same path but was never verified past whoami).
Session 2 — Synapse infra on parodia (touches live homeserver)
- Restore gogobee-registration.yaml: set
url:= bot tailnet addr, addreceive_ephemeral: true,org.matrix.msc3202: true, keepio.element.msc4190: true,rate_limited: false. - homeserver.yaml
experimental_features: msc2409_to_device_messages_enabled, msc3202_device_masquerading, msc3202_transaction_extensions, msc4190_device_management. Wireapp_service_config_files. - Determine bot's headscale tailnet address on parodia (Synapse must reach the bot's ListenPort). Confirm routing.
- Backup homeserver.yaml + registration; confirm before restart; restart matrix-synapse; check it comes back for all users.
Session 3 — cutover + E2EE verification on millenia
- Copy registration to bot host; set AUTH_MODE=appservice + AS_* env.
- rsync source + build in place (CGO=1 -tags goolm), restart screen session. Remove empty local data/gogobee.db first (empty_local_db_wipes_prod).
- Verify: transactions arrive (listener logs), whoami OK, MSC4190 device made,
bot responds to
!command in a PLAINTEXT room. - Verify E2EE: bot decrypts an incoming encrypted-room message AND its reply decrypts for a human client (encrypt-out works). This is the make-or-break.
- If broken: AUTH_MODE=masdevice, restart → back on device grant. Then triage.
Cleanup (after proven)
- Prune stale @twinbee devices, orphan MAS client, retire masauth.go or leave behind the flag. Fix HEAD mislabel (the eventual commit should actually contain device-grant + appservice, not the stale hybrid).