Status: canonical. Updated 2026-09-04.
This document describes how a WebSocket connection authenticates via
bbsengine6.auth and is then authorized to perform bank operations via
bbsengine6.bank. The two modules cooperate through a shared
in-process SessionManager; there is no per-message bearer token on
bank operations, and the auth handler binds a session id to a moniker
at auth login time so the bank handler can read that binding on every
request.
bbsengine6.auth (py/src/bbsengine6/auth/__init__.py)Pure authorization policy. Exposes a single access() function that
returns True/False for the four domain verbs:
| Op | Meaning |
|---|---|
login |
Issue a fresh bearer token (moniker/password) |
reconnect |
Rebind an existing token to a new WebSocket |
refresh |
Rotate the live session's token |
revoke |
Delete a token from the store |
The actual login (password verification, token signing, store write)
lives in bed/api/auth.py outside this repo. bbsengine6.auth.access
exposes the per-op policy decisions only.
bbsengine6.session.SessionManager (py/src/bbsengine6/session/lib.py)Process-local mapping:
session_id (int, monotonic) -> { "moniker": str, "is_sysop": bool }
Public methods:
| Method | Purpose |
|---|---|
alloc_session_id() -> int |
Issued by the transport on connect |
register_session(session_id, moniker, is_sysop=False) |
Called by the auth handler after login / reconnect / refresh |
unregister_session(session_id) |
Called by the transport on disconnect |
get_session(session_id) -> dict \| None |
Read-only lookup used by the bank handler |
bbsengine6.bank (py/src/bbsengine6/bank/__init__.py)Pure authorization policy for bank operations. Defines access() with
ops balance, add, remove, history, pending, transfer,
approve, reject, list_all. Reads message["claims"] first (HMAC-
verified bearer); falls back to session.moniker / session.is_sysop
when no claims are present.
bbsengine6.bank.api.handler (py/src/bbsengine6/bank/api/handler.py)WebSocket dispatch entry point. Maps incoming wire message types to
bank ops via OP_MAP, then delegates the per-op authorization decision
to bbsengine6.bank.access. The session id is id(websocket) --
the same value the transport hands to SessionManager.alloc_session_id().
SessionManager.alloc_session_id() -- returns an
integer session id.{"type": "auth", "op": "login", "moniker": "alice", "password": "..."}.bed/api/auth.py (consumer of bbsengine6.auth.access) verifies
credentials, asks
bbsengine6.auth.access(args, "login", session=None, message=msg),
signs a token, and calls
SessionManager.register_session(session_id, "alice", is_sysop).{"type": "bank_balance", "moniker": "alice"}.BankServiceHandler.handle_message._check_auth(args, "bank_balance", session_id, message, sessions)._check_auth looks up session_id in the SessionManager, wraps
the dict in SessionState, and calls
bank_access(args, "balance", session=..., message=...).bank_access returns True (alice owns her own account); the
handler proceeds with bank_service.get_balance("alice").router.unregister_session(session_id) and the bank router removes
the SessionManager entry.| WebSocket message type | Op | Required session state |
|---|---|---|
bank_balance |
balance |
self moniker == target, or sysop |
bank_add |
add |
self moniker == target, or sysop |
bank_remove |
remove |
self moniker == target, or sysop |
bank_history |
history |
self moniker == target, or sysop |
bank_pending |
pending |
self moniker == target (defaults to caller), or sysop |
bank_transfer_request |
transfer |
self moniker == from, or sysop |
bank_transfer_approve |
approve |
valid transfer_id, optional responded_by match |
bank_transfer_reject |
reject |
valid transfer_id, optional responded_by match |
bank_list_all |
list_all |
sysop only |
On denial the handler returns:
{"type": "error", "code": "forbidden", "message": "not authorized"}
When bed/api/auth.py verifies a bearer token for a request, it stashes
the decoded claims under message["claims"] before calling
bank_access. bank_access then prefers claim-derived
moniker / is_sysop over the in-memory session attributes because the
claims are HMAC-verified. The handler does not need to decode the token
itself; only the auth handler does.
| Condition | Effect on bank handler |
|---|---|
| WebSocket never logged in | SessionManager.get_session returns None; bank_access(..., session=None) returns False; handler returns forbidden |
auth login succeeded but register_session not called |
same as above |
| Session expired and transport did not unregister | stale session may still resolve until explicit logout; the refresh path is the safe way to rebind |
Token issued for session_id=A presented to WebSocket B |
bbsengine6.auth.access("refresh") denies; for bank ops, the in-memory session only reflects the current WebSocket, so a tampered session is invisible to bank_access |
| File | Role |
|---|---|
py/src/bbsengine6/auth/__init__.py |
Policy (bbsengine6.auth.access) |
py/src/bbsengine6/session/lib.py |
SessionManager |
py/src/bbsengine6/bank/__init__.py |
Bank policy (bbsengine6.bank.access) |
py/src/bbsengine6/bank/api/handler.py |
WS dispatch + _check_auth |
py/src/bbsengine6/net/transport.py |
Session id lifecycle |
py/tests/test_bank_access.py |
Unit tests for bank_access |
py/tests/test_auth_access.py |
Unit tests for auth.access |