Repository navigation
feat(worker): the first administrator signs in by a link from the log or a command (FRU-221) - #291
Conversation
… or a command (FRU-221) An instance that sends no e-mail and has no GitHub or Google sign-in could make no account at all. With FRUITBACK_FIRST_ADMIN=<address>, a worker whose instance has no owner yet writes a sign-in link for that address in its log when it starts; `admin-link --email <address>` writes one on the terminal. It is the link of an e-mail, sent nowhere, and it lands on the sign-in page of the console. - No request makes one. Ten minutes, spent once, one at a time: a new link ends the one before, and two replicas that start together write one. - Checked again when it is spent: refused once the instance has an owner, however they signed in. - The command also makes one for an owner who lost their session, refuses any other address, ends no session, and leaves a line in the log of the worker with the address and no link. - The token is on one line. The secret of the sign-in is asked for, and never made by the worker. - The console says to ask for a link when the worker has no mail and no provider. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Score de confiance : 4/5 · ✅ Rien de bloquant · Mergeable en l'état : aucun défaut certain n'a été trouvé dans le code visible, mais le lien de premier administrateur touche l'authentification et la concurrence, et deux implémentations de AccountStore et les appelants de kv ne sont pas dans le diff.
🔍 Une revue plus poussée est conseillée : Vérifier redeemLink et adminLinkAt contre les appelants réels et les autres implémentations de AccountStore, puis le partage du Kv entre réplicas, car le lien touche l'authentification et la concurrence.
Commentez @sakuga-review review balanced (environ 5 centimes) ou @sakuga-review review heavy (10 centimes à 1 dollar).
Niveau lite · anthropic/claude-haiku-5.5 · 0,012 $ · Le résumé, le diagramme et la liste des constats sont à la fin de la description de la PR.
|
@sakuga-review review heavy |
There was a problem hiding this comment.
Score de confiance : 4/5 ·
Niveau heavy · anthropic/claude-opus-5.5 · 0,540 $ · Le résumé, le diagramme et la liste des constats sont à la fin de la description de la PR.
| 'Read the log of that start, wait and start again, or run `node server.mjs admin-link --email <address>`.', | ||
| ]; | ||
| } | ||
| const link = await mintAdminLink(links, { kind: 'first', email, at: now }); |
There was a problem hiding this comment.
Compteur de démarrages incrémenté même si aucun lien n'est créé
Au démarrage, incr(STARTS) renvoie 1, puis mintAdminLink échoue : better-auth n'est pas prêt, ou le Kv.set rejette. Aucun lien n'est écrit, mais le compteur reste à 1. Pendant dix minutes, chaque redémarrage écrit qu'« un lien a été écrit il y a moins de 10 minutes », alors qu'il n'existe nulle part.
Correction : Si mintAdminLink échoue, décrémenter ou effacer STARTS avant de relancer l'erreur. On peut aussi n'incrémenter qu'après une création réussie, avec un autre verrou pour les réplicas.
There was a problem hiding this comment.
Right, fixed in e8a717a. When the link cannot be made, the count of the starts goes back before the error goes up, so the next start writes a link. test:does not say that a link was written when the start that should have written it failed starts once with a better-auth that does not start, then again: the second start writes a link, and it signs in. Without the reset, that test fails.
There was a problem hiding this comment.
Ce constat n'apparaît plus au commit e8a717a88, et le fichier a changé depuis. Le fil peut être résolu.
| {closedByOperator | ||
| ? t( | ||
| 'This Fruitback sends no sign-in link by e-mail. The person who runs it makes one for you: ask them for a sign-in link, then open it in this browser.', | ||
| ) |
There was a problem hiding this comment.
Le message promet un lien que la commande refuse
Une instance a déjà un propriétaire, pas d'e-mail et pas de fournisseur. Un membre invité lit « The person who runs it makes one for you: ask them for a sign-in link ». L'opérateur lance admin-link --email membre@…, et adminLinkFor refuse toute adresse qui n'est pas un propriétaire. La console oriente donc ce membre vers une voie qui n'existe pas pour lui.
Correction : Restreindre le texte au premier administrateur et aux propriétaires, ou dire qu'un membre ne peut pas se connecter tant que l'instance n'envoie pas d'e-mail ou n'a pas de fournisseur.
There was a problem hiding this comment.
Right, and it shows a limit that is worth saying out loud: on an instance with no e-mail and no provider, only an owner can sign in. A member who was invited has no way in, since the command refuses every address that is not an owner (arbitrated: never a second owner, nobody promoted). Fixed in e8a717a: the sentence no longer tells anybody to ask for a link. It says « This Fruitback sends no sign-in link by e-mail. Only its owner signs in for now, with a link that the person who runs it makes. Everybody else can sign in once it sends e-mail, or offers GitHub or Google. » Whether the command should also make a link for an existing member (it would sign them in, and promote nobody) is a question for Mathieu; I put it at the top of the description.
… console does not send a member to ask for one (FRU-221) Raised in review. - The count of the starts stayed at one when the link could not be made: every start of the next ten minutes then said that a link was written, and there was none. The count goes back on a failure. - The sentence of the sign-in screen told anybody to ask for a link, and the command makes one for the first administrator and for an owner only. It now says that only the owner signs in for now. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Score de confiance : 4/5 · ✅ Rien de bloquant · Mergeable après revue humaine : aucun défaut certain n'est ressorti de la lecture du diff, mais la logique de liens à usage unique (marques dans le Kv, concurrence entre réplicas, contrôle à la consommation) n'a pas pu être vérifiée jusqu'au bout depuis ce seul message.
🔍 Une revue plus poussée est conseillée : Vérifier dans redeemLink et adminLinkAt l'ordre des contrôles de marque et la concurrence entre deux réplicas sur le Kv partagé, car ce chemin d'authentification n'a pu être confirmé qu'en partie.
Commentez @sakuga-review review balanced (environ 5 centimes) ou @sakuga-review review heavy (10 centimes à 1 dollar).
Niveau lite · anthropic/claude-haiku-5.5 · 0,012 $ · Le résumé, le diagramme et la liste des constats sont à la fin de la description de la PR.
|
@sakuga-review review heavy What changed since the heavy review of e7e3def (4/5, two P2): one commit, e8a717a, their two fixes. |
There was a problem hiding this comment.
Score de confiance : 4/5 · ✅ Rien de bloquant · Fusion possible : aucun défaut bloquant n'apparaît dans le flux du lien du premier administrateur. Quelques dépendances restent non vérifiées dans le message : hasAuth, kvFor, sendLinkThrough et la sémantique de Kv.incr.
Niveau heavy · anthropic/claude-opus-5.5 · 0,566 $ · Le résumé, le diagramme et la liste des constats sont à la fin de la description de la PR.
…r-un-compte-dadministration-toujours-possible # Conflicts: # apps/worker/src/cli.ts # apps/worker/src/env.ts
There was a problem hiding this comment.
Score de confiance : 4/5 ·
Niveau lite · anthropic/claude-haiku-5.5 · 0,013 $ · Le résumé, le diagramme et la liste des constats sont à la fin de la description de la PR.
|
Reply to the finding of the The sentence holds without This head (fd73733) is only the merge of main; nothing changed in this file. |
For Mathieu: a new way in to the console, with no proof of the address. I do not merge it. Base
main. No migration.Two points to confirm first
The command also gives a link to an owner who lost their session. The arbitration of 2026-10-13 (on delegation of Mathieu) says: a link only while the instance has no owner, never again after. I applied that to the log, to the letter. For the command I kept what the ticket asked, with the coordinator's agreement, and it is yours to confirm: a session of the console lives seven days, and an instance with no e-mail and no provider has no other way back in. The command makes a link for an instance with no owner, or for the address of an owner; it refuses every other address. It never makes a second owner, promotes nobody, ends no session, and leaves a line in the log of the worker (who and when, never the link). It needs a shell in the container, which already reads the database. Taking the recovery out is one condition.
« No owner », and not « no account » as the ticket wrote. A person who opened the link and closed the browser before the workspace was made is an account that owns nothing: with « no account » that instance could never get a link again.
On an instance with no e-mail and no provider, only an owner can sign in. Found in review. A member who was invited has no way in: the command refuses every address that is not an owner. The sign-in screen now says so, in place of telling everybody to ask for a link. Should the command also make a link for a person who is already a member? It would sign them in and promote nobody; it was not arbitrated, and it is not in this pull request.
What it does
An instance that sends no e-mail and has no GitHub or Google sign-in could make no account at all (FRU-217 made it say so). Mathieu, 2026-10-13: an account is always possible for whoever administers.
FRUITBACK_FIRST_ADMIN=<address>: a worker whose instance has no owner yet writes a sign-in link for that address in its log when it starts.node server.mjs admin-link --email <address>: the same link, on the terminal.sendLinkThrough, as the route of the e-mail). It opens<console>/sign-in#<token>, and the person goes on through the setup of feat(console): the setup on one screen: your account, your site (FRU-239) #267. No screen of its own, no route, no component asked of the kit.sign-in-form.tsx,api.ts,messages.fr.ts.FRUITBACK_FIRST_REVIEWERstays refused on a worker with workspaces, with the same sentence (pair from the console, orpair --workspace). Once the database is required (FRU-225) it has no valid configuration, and goes with the SQLite sessions (FRU-226). Written inworker.md.FRUITBACK_FIRST_ADMINwithoutFRUITBACK_AUTH_SECRETrefuses to start, and says that the worker does not make it.docker-compose.yml,.env.example,docs/self-hosting.md(« The first administrator »),SECURITY.md,docs/decisions/worker.md.The threat model, and the test that holds each line
apps/worker/src/first-admin.test.ts, 19 cases, through the routes of the console on PostgreSQL (PGlite) with better-auth on and no mailer.SECURITY.md. Ten minutes (ADMIN_LINK_TTL_SECONDS, the number of FRU-108) where the link of an e-mail lives fifteen; spent once; and its use is written in the log, so an operator who did not open it reads that somebody didis refused after ten minutes, where the link of an e-mail lives fifteenKv(in the database, shared) counts the starts of a window: the first writes the link, the other says that one was written. And one link is current: a new one ends the one beforeis one link when two replicas start together, and the other one says that it was written,cancels the link before with each new one, whoever asked for eachis spent once: the same link opened again is refusedis refused once somebody else owns a workspace, however they signed in(and it stays refused when that owner leaves)#, which no browser sends: no access log and no proxy. No line of the worker about a use or a refusal holds it, and no table of the databasewrites its token on one line of the log, and nothing else of the worker ever writes itGETandPOST, with and withoutOrigin, on a worker that names a first administrator, and reads that no mark was written; its control writes twowhatever the path, the method and the body, on a worker that names a first administratorwrites nothing once a workspace has an owner: the variable left in the environment opens nothingowner,admin), and a strangerrefuses every other address once there is an owner: no second owner, and nobody promotedgives an owner who lost their session a link, and ends no session that is openrefuses the link of an owner who is no owner any more when it is openedmakes no link when the store of the marks does not answer, and spends noneTwo holes that the tests found while I wrote this, both fail-open. A link whose mark is gone is the link of an e-mail, and would be spent with no check. The mark first lived ten minutes: an eleven-minute-old link signed in. Then a refused link had its mark removed, and better-auth, which had not been asked, still held it: the second try signed in. So the mark holds the moment of the link, outlives it, and is never removed (« ended »).
worker.mdsays it.Played backwards, each check taken away in turn: the age → 1 test fails; « current » → 2; the owner at the spend → 1; the mark removed → 1; the count of the starts → 1; the command taking any address → 1; the log writing whatever the owners → 1.
Proof
e2e/console-first-admin.spec.ts, against a worker on PGlite with no mailer. The sign-in screen says to ask for a link and shows no form; the link of the log opens the console signed in, the workspace is made, the setup is at « Your site »; a second start writes nothing; the link in another browser is refused.e2e/console-no-sign-in.spec.tsholds the sentence on/sign-inand/setup, and its control (a provider keeps the form).FRUITBACK_SCALEWAY_TEM, no provider):docker logs;/auth/providersanswered{"betterAuth":{"email":false,"github":false,"google":false}};Originof the console:200, then/console/me200,POST /console/workspaces201; the same link again401; the log said « the sign-in link made for … was used »;docker exec … admin-link --email <the owner>: exit 0, « sign-in link for …, an owner », anddocker logsgot « a sign-in link was made by the command admin-link for … (an owner) » with 0 line holding its token; the session of before still answered200; the link signed in;admin-link --email <somebody else>: « is not one … never makes another owner », exit 1;FRUITBACK_AUTH_SECRET:/health503, naming it.test:postgres1038, 0 fail; the console tests 107 pass;typecheck,oxlint,oxfmtclean.The screen, at 1440 and 390,
/sign-inand/setup: captures inagent-coord/scratch/fru-auth/221-shots/(they cannot be uploaded). One sentence in the existing alert of FRU-217 (Problem): no new element, no value, no token of the kit. French: « Ce Fruitback n’envoie pas de lien de connexion par e-mail. Seul son propriétaire se connecte pour l’instant, avec un lien que crée la personne qui l’exploite. Les autres pourront se connecter quand il enverra des e-mails, ou proposera GitHub ou Google. » (The captures show the sentence of the first commit; the element is the same.)Not done, not verified
docker compose up, the link opens the console »: the console is not in the compose file and its image is not published (FRU-222). The image run above spends the link withcurland theOriginof the console; the browser run is on PGlite./proc/1/fd/1. Measured in the image with Docker on macOS. Not tried: Podman, a container whose first process is not the worker, a read-only/proc. The command says so when it could not write.Kvin memory; theKvof PostgreSQL has its own concurrency tests (FRU-67).🤖 Generated with Claude Code
Sakuga Review · Score de confiance : 4/5
Mergeable après un regard sur le finding mineur : le mécanisme de lien (marques dans le Kv, lien courant, contrôle au moment de la consommation) tient dans le code lu. Le seul écart relevé est un texte affiché dans un cas où personne ne peut encore se connecter.
Constats
apps/console/app/api.ts:308Sur une instance sans e-mail ni fournisseur, byOperator vaut vrai dès que le worker répond betterAuth sans mail ni GitHub ni Google. Le texte dit alors que seul son propriétaire se connecte avec un lien de l'exploitant, même si aucun propriétaire n'existe et que FRUITBACK_FIRST_ADMIN n'est pas défini. Le visiteur croit qu'un lien va arriver et n'a aucune issue. Correction : Ne pas dériver byOperator du seul état du worker : n'afficher ce texte que si le worker dit qu'un propriétaire existe ou qu'un administrateur est configuré, sinon garder le message de configuration.
Résumé
La PR ajoute un lien de première connexion pour un instance sans e-mail ni fournisseur : le worker l'écrit dans son journal au démarrage (FRUITBACK_FIRST_ADMIN) ou via la commande admin-link, tant qu'aucun espace n'a de propriétaire. Le lien est marqué dans le Kv par son empreinte, il ne vaut qu'une fois, dix minutes, et il est refusé dès qu'un propriétaire existe. La console affiche alors un message à la place du formulaire. J'ai relu les routes de consommation, la configuration, le store (Postgres et SQLite), la commande et le composant d'interface. Je n'ai pas pu vérifier kvFor ni le contenu exact de sendLinkThrough.
Diagramme
%%{init: {'theme': 'neutral'}}%% flowchart LR A["server.ts"] --> B["announceFirstAdmin"] B --> C["firstAdminLines"] C --> D["mintAdminLink"] E["auth-routes.ts redeemLink"] --> F["adminLinkAt"] F --> G["owners()"] E --> H["endAdminLink"]Non relu
Revues (5) · Dernier commit relu : Merge remote-tracking branch 'origin/main' into... · Niveau
lite·anthropic/claude-haiku-5.5· 0,013 $