Files
gerbeur/api/test/services
khannurien c0d03e9aaa v4: users sign in to one gerbeur instance with their account on another — topic instances, each with its own admins, domain and data, had no way to share accounts. Sign-in is federated over a subset of OpenID Connect, the authorization code flow with PKCE: each instance is an identity provider for its own users and a client for the others', and signs what it tells them with an Ed25519 key kept in federation_keys and published at /.well-known/jwks.json, beside its metadata at /.well-known/openid-configuration. Only sign-in is federated: an account from another instance is a local users row linked to (issuer, subject) in user_identities, with no password (password_hash ''), so sessions, roles, permissions, the socket and every foreign key work as before.
A remote user is name@host here, in users.username, in paths (/@alice@music.example) and in mentions, through one handle rule in shared/validation.ts that paths, MENTION_RE and Markdown all build on; Markdown undoes GFM's email autolink of a handle. Their name, email and avatar come from their home at each sign-in and by its signed events (federation_outbox, delivered by the federation job with backoff): a rename at home leaves the old handle an alias here, and a password change or a withdrawn grant ends their sessions here. Their password, email, avatar and name can't change here (assertLocalUser), nor can they sign in elsewhere from here.

An admin keeps the trusted and blocked instances on /manage (federation:manage): a trusted instance's users sign in from a button on the login page, a blocked one's can't and are signed out at once, and any other's join with an invite from the register page. The /authorize page asks the user once per instance, and /settings lists the instances their account signs in to, each disconnectable; /login?next= brings a signed-out visitor back to /authorize. Emails stay unique among local accounts only, and migration 0024 adds the tables, makes that index partial and reserves authorize and federation as category slugs. remote/gerbeur-instance.ts is the only way to other instances: https and public addresses only, no redirects, no certificate fallback, unless GERBEUR_FEDERATION_ALLOW_PRIVATE lets two instances on one machine talk.

298 backend tests and 702 frontend tests pass, and deno check, deno fmt, deno lint, tsc, eslint, doc:lint and i18n:compile --strict are clean; checked end to end with two built instances on one machine: sign-in through the other's login and consent pages, consent skipped once granted and asked again after disconnecting, a rename and a password change at home reaching the other, and blocking; screenshots of the login, consent, profile, settings and manage pages at 1280 and 390px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:23:00 +00:00
..
v4: a moderator's or an admin's @everyone in the chat notifies every user — there was no way to reach everyone at once. The new chat:everyone permission, held by moderators and admins, lets a chat message saying @everyone notify every user but its author and those reading the chat, through notifyEveryone, after notifyMentions, so a user also named is told once, by name; the notification is a user_mentioned with everyone set, which the notifications page and the digest word as "mentioned everyone in the chat". chat_messages.mentions_everyone, sent as ChatMessage.mentionsEveryone, is set from the author's role each time the body is written, so a user's @everyone reaches no one, a moderator's edit of a user's message can't make it reach everyone, and a demoted author's next edit loses it. Only an edit that adds @everyone notifies, so fixing an announcement's typo reaches no one new. The client highlights and chimes by the flag, never by the text; Markdown shows @everyone as a mention only in a flagged message, and never links it to a profile. The chat composer offers @everyone first to those who may say it. No account may be named everyone, in any case: migration 0023 adds the column, flags the moderators' and admins' past @everyone messages, renames a user named everyone to everyone_N and drops an alias everyone. 265 backend tests and 681 frontend tests pass, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean; checked in screenshots of the chat at 1280 and 390px, with a moderator's and a user's @everyone and the suggestions open.
2026-10-09 12:54:09 +00:00
v4: a /community page shows the site's week and month, from the same figures as the digests, and every recurring server task is a job of one scheduler — the digests' top dumps and community section (who joined, the new public playlists, the chat and the totals) existed only in emails. community-service.ts now gathers them for a period from listTopDumps, listNewUsers and listNewPublicPlaylists, each in its own service and returning the API's Dump, PublicUser and Playlist; the digest builds its sections from the same call, its email-only DumpRow and DUMP_COLS replaced by one Dump mapper that networkOf uses too, and links its community section to the page. The page, /community for the last week and /community/monthly for the month, signed in only and linked from the avatar menu, reads GET /api/community?period=, which serves figures kept in memory and recomputed every GERBEUR_COMMUNITY_REFRESH_MINUTES (15 by default). It shows the four figures, the top dumps and the new playlists as live cards, and the new members as UserCards, the follow tab's user row made shared. Its words are the emails' own: the messages both use are renamed from email.digest.* to digest.*, as digest.weekly.off was. api/lib/scheduler.ts's startJobs runs the digests, the community refresh, the WebSocket ping, the typing expiry and the rate-limit sweep, which main.ts starts, so none of them runs on import any more. PERIOD_MS moves to shared/settings.ts. community is reserved as a category slug, and migration 0022 renames a category that had it, with renameReservedCategories moved out of 0021 into reserved-slugs.ts. A group's dump or playlist cards now fill its column. 262 backend tests and 678 frontend tests pass, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean; checked in screenshots in Smooth light at 1280 and 390px, Smooth dark with mocked playlists, Brutalist dark at 390px and Kanagawa dark at 1280px.
2026-10-09 11:42:01 +00:00
v3: fix the backend's correctness findings — each by a rule shared across the code rather than one more special case. Link previews: readTextCapped decoded every page as UTF-8, so a Latin-1 page came out garbled unless it went through the curl fallback, which re-encoded it on its own; pages are now decoded in the charset their Content-Type or <meta> declares, and the curl fallback hands the bytes over as they are. The image proxy served and cached for a day an error answer that claimed an image type, and read a body without Content-Length whole before checking its size; it now refuses an error status and reads through the same capped reader. The icons task's Twemoji fetch gives up after 15 s instead of holding the start up, and extractLinkHref, which only its tests used, is gone. Backlinks matched a linked URL against the raw url column, so https://x.com/ missed a dump posted as https://x.com; they now compare canonical URLs, as reposts do. The four LIKE searches put the query in raw, so % and _ were wildcards and the mention search for "a_b" found "axb"; lib/like.ts escapes them. Every list query now treats a malformed row the same way, a 500 through model/rows.ts's parseRows, where ten of them silently dropped it; the playlist dumps query uses the model's column list rather than a hand-written copy, and the aliased list is derived from the plain one. The vote and like services commit through transaction() instead of their own BEGIN and ROLLBACK, which would have rolled back a caller's transaction.
2026-09-29 19:35:53 +00:00
v3: api/ gets one layout rule and keeps to it, and whatever crosses the wire is defined once, in shared/ — api/model/interfaces.ts and src/model.ts declared the same 40 types by hand and had drifted: the error envelope, email required on one side and optional on the other, Invitee/InviteTreeEntry and LinkPreview/UrlPreviewResponse for one shape each. The validation limits, the role permissions, the chat length cap, the @mention pattern and the Bandcamp and YouTube player responses were copied too, behind "keep in sync" comments. shared/ now holds them once, one file per domain (api, category, chat, comment, dump, follow, mention, notification, permissions, player, playlist, search, user, validation, ws), and imports nothing from either half. The server sends Dates; the client reads Wire<T>, the same type with every Date an ISO string, and turns it back with src/utils/shared/deserialize.ts. APIErrorCode becomes a const object, since the frontend's erasableSyntaxOnly rejects an enum. The shared types showed two real gaps: the avatar upload and GET /api/users/me return the full self user and the frontend read them as public users, and a stored session may predate media tokens, so the client keeps it as its own Session type. The backend rule: each folder holds one kind of module, each file is named after its domain. routes/ parse and answer; services/ hold the logic and every SQL query (lib/auth.ts's token check moves to user-service); remote/ is the only place that fetches from other sites — the guarded fetch and the HTML scraping out of rich-content-service, public-address, the Bandcamp and YouTube resolvers, the link providers, the image proxy (fetch, checks and cache, out of routes/preview.ts) and the Twemoji glyphs (out of lib/site-icons.ts). model/ holds per table a row type, its guard and its mappers; validators/ the request guards; db/connection.ts the handle and transaction(), db/startup.ts the purges and default admin that db.ts ran on import, called at the same moment. Tests mirror the source one file per module, test/<layer>/<module>.test.ts, with shared/'s under shared/test/ (deno task test runs both); the test that covered the bandcamp and soundcloud providers together is two now, and the backend can() has its own, so 202 tests. deno lint src/ is clean too: window.* becomes globalThis.*, and test bodies and act() callbacks that never awaited lose their async. The README states the rule. No behaviour changes; the catalogues only move their source references.
2026-09-27 20:19:48 +00:00