Skip to content

Mini App API: auth, group config, audit trail — plus the brand and contributing docs - #11

Merged
dangerousplay merged 11 commits into
mainfrom
docs/brand-and-contributing
Aug 15, 2026
Merged

dangerousplay merged 11 commits into
mainfrom
docs/brand-and-contributing

Conversation

@dangerousplay

Copy link
Copy Markdown
Collaborator

Two things rode this branch: the docs work it was named for, and the Mini App API built on top of it.

The API a Mini App can use

  • x_miniapp_auth — /oauth2/token with three grants (Telegram initData, the login widget's payload, refresh_token) and /oauth2/revoke. Same RS256 keys and JWKS /login already publishes, so one resource server verifies both; what is new is scope, aud, a fifteen-minute access token and a refresh token that rotates on every use. A replayed refresh token revokes its whole family. /login is untouched and its scope-less token stays read-only.
  • x_group_config_api — every /config setting plus the rules and welcome text, readable and writable over HTTP by a group's admins. groups:read / groups:write, PATCH semantics so two admins do not overwrite each other, and 404 (not 403) for a group you do not administer, so a logged-in stranger cannot walk chat ids.
  • x_audit_log — every settings change leaves a row, from both surfaces. group_audit_events is distributed on group_id with a UUIDv7 key, so the newest-first page is a keyset read on one shard. A failed audit write never fails the action it describes.

Migration 0010 adds both tables: refresh_tokens as a reference table storing SHA-256 hashes, group_audit_events distributed like everything else a group owns.

OpenAPI

Every endpoint the Mini App calls now names a response model instead of answering type: object, the token endpoint declares its request body under both content types it accepts, and the refusals (401 / 403 / 404 mean three different things here) are documented per route. test_openapi.py keeps it that way — the path list is a whitelist, so a new endpoint either describes itself or fails the suite.

Docs

The Mini App API page, a page per feature family under Using Cookiebot, the contributing guide and issue forms, the avatar's palette as design tokens, and the board moving to 65/65.

Checks

python scripts/cb.py check green locally: 3391 passed, 44 skipped; mypy clean on 174 files; the compiled-module bench clears its gate; spec, docs and tests agree in both directions.

🤖 Generated with Claude Code

https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s

dangerousplay and others added 11 commits August 14, 2026 23:08
…nlisted

The spec row carried an empty trigger tuple, so the parser's three spellings
(/transcribe, /transcrever, /transcribir) resolved to no feature at all and the
generated command tables reported the command as "Coming" while it had been
live since M3.

Regenerates sandbox.config.json and the progress data off the corrected spec;
`cb.py status` reports no inconsistencies in either direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
The docs site looked like a Fumadocs default while the bot and the web hub
share a face and a colour scheme. `app/theme.css` now holds one ramp, sampled
from the avatar (cocoa #382602, fur #9C834D, blush #FFB5D8) and from the hub's
globals.scss (gold #C48B17, caramel #955714, cream #F5D9BC, its status trio),
plus the hub's three type faces in the same roles. Fumadocs' --color-fd-*
variables are remapped onto those tokens for both themes, so no component
spells a hex code any more.

- brand assets reused from COOKIEBOT-WebHub: the avatar as the nav mark, the
  landing hero and the README header; the circuit ornaments masked to a token
  colour rather than pasted in as artwork
- `asset()` prefixes NEXT_PUBLIC_BASE_PATH: a static export does not rewrite
  <img src>, so an unprefixed asset 404s only on Pages
- status colours get -ink variants; the hub's fills are tuned for dark
  backgrounds and drop under 4.5:1 as small text on cream
- <CommandCards>, the card rendering of the generated alias table, for pages
  that explain a command rather than list it
- OG cards rendered in the same palette instead of Fumadocs' black

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
…we told groups

The user-facing section covered setup, the config menu, moderation, privacy and
troubleshooting — six pages for a bot with 48 commands, so most of what it does
was documented only as a row in a generated table. It now has one page per
family (welcome/rules/language, fun, utilities, AI, giveaways and events,
cross-posting, owner commands), written from the Gherkin scenarios and the
handlers rather than from memory.

Reading those sources corrected four things the site had wrong:

- sticker floods are counted per *group*, not per member, and admins are not
  exempt — one person's flood gets the next person's sticker deleted
- /dice obeys Utility Functions and /birthday obeys Fun Functions, which is
  where v1 put them
- an unrecognised command is not silence: it becomes an image search
- privacy now lists what actually leaves the deployment (the model provider,
  transcription, reverse search, agify/genderize, the anti-spam services)

The README follows the same shape: what the bot does first, the rewrite second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
… loop

Reporting a problem meant a blank issue box, which reliably arrives without the
command, the deployment or what the person expected. Four forms replace it, each
asking for two or three facts and nothing more: something is broken, v2 behaves
differently from v1, an idea, the docs are wrong. Blank issues are off; the
config links the docs, the troubleshooting page and the private advisory form,
so a security report has an obvious place that is not a public issue.

The parity form exists because "a command changed its wording" is the most
valuable report this project gets and had nowhere to go.

CONTRIBUTING.md says the part that was never written down: reports and ideas are
contributions, half-formed ideas are welcome, and running a large group makes
you the only person who can see certain defaults being wrong. It then documents
the loop — branch, scenario first, implement, spec, docs, gate, PR — and the
Conventional Commits shape cliff.toml already assumes.

The development guide gains the same loop and the commit-message table, since
that is where a contributor is standing when they need it, and a pull request
template asks the three questions review always asks anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
Two tables one shard rule apart. `refresh_tokens` is a reference table — a
session is not a group's data and any node may redeem one — and stores SHA-256
hashes, so a database dump is not a set of live sessions. `group_audit_events`
is distributed on `group_id` like everything else a group owns, keyed by a v7
UUID, which makes the newest-first page a keyset read on one shard with no
second index on `ts`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
`/login` authenticates a login-widget payload, which a page opened inside
Telegram does not have. It has `initData`. So `/oauth2/token` takes both, as
two extension grants, plus `refresh_token` — one endpoint, RFC 6749 error
bodies, and RFC 7009 revocation beside it.

The access token is the same RS256 JWT `/login` mints, signed by the same keys
and published by the same JWKS, so nothing downstream learns a second format.
What is new is `scope` and `aud`, a fifteen-minute life, and a refresh token
behind it that rotates on every use: presenting a spent one revokes its whole
family, because there is no way to tell a buggy client from a thief and letting
both refresh is the worse failure.

`/login` is untouched. Its token carries no `scope`, and `security` reads that
as read-only — exactly what the shipped console could do before this existed.
The discovery document gained the token endpoint, the grants and the scopes;
its five v1 keys are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
v1 kept no trail at all, which was survivable while the Telegram menu was the
only way to change a setting. It is about to stop being the only way, and a
settings surface with two front ends and no history is how a group ends up
unable to answer that question.

Every write records the fields that actually moved, with their old and new
values, the actor and the surface — `telegram` for the `/config` menu,
`miniapp` for the HTTP API — so one trail reads the same whichever way an admin
changed something. An anonymous admin produces a row with no actor: Telegram
hands the bot GroupAnonymousBot, and a guess would be worse than a gap.

A failed audit write never fails the action it describes. The setting changed;
losing the evidence of it is bad, and undoing it because the evidence was lost
is worse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
Everything `/config` can do in a private chat, an admin can now do from the
Mini App: read the group's settings, change them, set the rules and the welcome
message, read back who changed what.

Three rules shape it. The group is the authorisation boundary — every path
carries `{group_id}`, and a caller who does not administer it gets 404, the
same answer an unknown group gets, so a logged-in stranger cannot walk chat
ids. Reading and writing are different scopes, which is why a scope-less
`/login` token stays read-only. And every write leaves an audit row.

`PATCH`, not `PUT`: a form that round-trips every column would overwrite the
change another admin made while it was open. The rules and welcome text go
through the same `cb_core.group_texts` upsert `/newrules` and `/newwelcome`
now use, so a body set here shows up in the chat immediately — the handlers
were rewritten onto it rather than given a second table to disagree with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
The Mini App will be built from a generated client, and a generator is only as
good as the document it reads. Every route it touches answered `type: object`
— true, useless, and an invitation to type the shapes out a second time by
hand and let the two drift.

So each one now names a model: the token and error bodies, `/me`, the config,
rules, welcome and audit responses, and the four analytics reads. The token
endpoint's request body is declared under both content types, because it takes
form-encoded *and* JSON and a schema that admitted only one would be a lie
about what the handler accepts.

The refusals are documented too — 401, 403 and 404 mean three different things
here, and a client that cannot tell "log in again" from "you do not administer
this group" will retry the wrong one.

`test_openapi.py` holds the line: the path list is a whitelist, so a new
endpoint either describes itself or fails the suite. `/` and `/login` stay
exempt; a response model on either would filter v1's error bodies back out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
Three features enter the spec — `x_miniapp_auth`, `x_group_config_api`,
`x_audit_log` — so the progress board, the feature index and the README's
counts move with them rather than being edited by hand later.

The Mini App API page is the one a client author reads end to end: the token
flow with its three grants, what the access token carries, every endpoint
behind it with its scope, the audit trail's shape, the error table, and the
operator's settings. It points at `/openapi.json` for the exact types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136T2HD8LrV65kGzZfhiM3s
@dangerousplay
dangerousplay merged commit 04e2331 into main Aug 15, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant