# Sign in with Nano: implementation spec

Follow these steps in order. Each step ends with a check. Stop and ask the site owner when a step says "ask".

- Guide: https://nano133.com/guides/sign-in-with-nano · as Markdown: https://nano133.com/guides/sign-in-with-nano.md
- Stage: **Alpha**. We ran its code and its checks pass. It read a real node (read only), or, if it needs no node, it ran on real signatures or real files. Where it copies code that nano133.com runs, a test shows both act the same. Last checked 2026-10-25. No node needed. Test script with real signatures: passed. A second test shows it acts the same as the sign-in code of nano133.com.
- Use at your own risk, for education.
- Code files: https://nano133.com/guides/sign-in-with-nano/code/purse.mjs · https://nano133.com/guides/sign-in-with-nano/code/signin.mjs

## Rules that must hold

1. The signed text always names the domain of the site. Never sign or accept a text that does not.
2. Never change the texts or the version byte of the passkey scheme after one person has made a purse. Add a new version instead. Keep the scheme in one file and test it with a fixed vector (the example below has one).
3. A seed never leaves the person's browser. Never post it, store it, log it or put it in a URL.
4. Check the whole address (shape and checksum) before anything else. Refuse weak keys and the second form of a signature. Build the signed text on the server from your own domain.
5. Count a sign-in once, by the hash of the signed text, in a write that fails when the key exists.
6. Make a new session at every sign-in. Learn who is signed in only from the session. Check the origin on every route that changes something, and rate-limit the sign-in route.
7. A wallet app's request can be sent to someone else, who approves it by mistake. Do not take such a sign-in as proof for an action that cannot be undone. nano133.com marks a sign-in made this way, and the page that deletes a person's data shows how the person signed in (“signed in with NanChat 3 minutes ago”) before it goes on.

## Steps

The guide has the how for each step. This list has what must be true at its end.

1. **Decide what is signed.** Check: run `node signin.mjs message example.com <an address> 1790000000000`: it prints the text above. Another domain, or another time, prints another text.
2. **Make a purse from a passkey.** Check: run `node signin.mjs passkey-address 1,2,3,…,32` (the numbers 1 to 32): it prints `nano_116an8efpjddh38hrkpjgbrbbrmu6brcrxbqkg63gmuj3aziejhac7hy9bfy`, every time. A short answer, or 32 equal bytes, makes no purse.
3. **Sign the text.** Check: run `PURSE_SEED=<a throwaway seed> node signin.mjs sign example.com`: it prints `address`, `at` and `signature`, and nothing else. The same seed, text and time give the same signature.
4. **Check the signature, strictly.** Check: sign for `example.com` and check it for `example.com`: `signed in as nano_…`. Check the same body for `other.example`, or with another account in `address`: `the signature does not match`. An address with a wrong checksum, the burn address and the second form of the signature are each refused with their own answer.
5. **Count each sign-in once.** Check: post the same body twice: the first signs in, the second answers `that sign-in was already used`. A body that is 6 minutes old, or 6 minutes ahead, answers `too old`.
6. **Make the session.** Check: sign in twice: two different cookies, each with `HttpOnly`, `Secure` and `SameSite=Lax`. `whois` with a cookie answers the account; with the stored hash, or with a made-up value, it answers `nobody`. A request with an `Origin` that is another site, `null`, or a `Sec-Fetch-Site` of `cross-site` or `same-site` is refused.

## Done when

- [ ] Step 1 check passes.
- [ ] Step 2 check passes.
- [ ] Step 3 check passes.
- [ ] Step 4 check passes.
- [ ] Step 5 check passes.
- [ ] Step 6 check passes.
- [ ] `node test.mjs` passes (put the code files next to it). It needs no node and sends nothing.
- [ ] You told the owner the stage of this guide and of the track you used.

## What breaks, and what it costs

| Failure | Loss in Nano | Check |
| --- | --- | --- |
| The passkey scheme changes after people made purses | Every person gets another account. The XNO stays in the old purse, and the passkey opens it again only with the old texts | Step 2: freeze the scheme, keep it in one file, test a fixed vector |
| A person loses the passkey | The purse and every XNO in it, unless the person kept the seed | Step 2: show the seed as a spare key before any XNO goes in |
| A seed reaches your server, a log or a URL | Everything the purse holds | Step 3: the body holds no seed, and you never log a seed |
| The signed text does not name your domain | None in Nano. A signature made for another site signs in to yours | Step 1: the text names the domain; the server builds it |
| An address is taken without its checksum | None in Nano. One key appears as many accounts, and the records split | Step 4: the address is whole before anything else |
| A weak key, or the second form of a signature, is accepted | None in Nano. Someone signs in to an account that nobody holds, or one sign-in counts twice | Step 4: refuse both, each with its own answer |
| A sign-in is accepted twice | Anyone who saw a sign-in can use it again for 5 minutes and be signed in as that person | Step 5: one unique key for each signed text |
| A page on another site posts a sign-in (login CSRF) | None in Nano. A person is signed in to an account that is not theirs, and may pay it | Step 6: check `Origin` and `Sec-Fetch-Site` |
| A wallet app's request is approved by the wrong person | Someone else's browser is signed in as that person | The wallet apps section: never use such a sign-in as proof for an action that cannot be undone |

## Notes for agents

Ask the site owner for these five things before you start. Never guess them:

1. The domain, exactly as people see it (`example.com`). The signed text names it, and so does the passkey.
2. The passkey scheme: the three texts and the version byte. Write them once, put them in one file marked as frozen, and ask the owner to approve them. They never change after the first person makes a purse.
3. Where the used sign-ins and the sessions live: a database with a unique key, a time-to-live on the rows, and the account on each row.
4. The rate limit of the sign-in route, and the list of routes that change something (each must check the origin).
5. Whether the site needs the wallet apps (Nautilus, NanChat, Berry Wallet) beside the passkey purse.

Never ask for, print or store a seed. The server never has one. Do not build a sign-in by payment. Test with the throwaway seeds that `node test.mjs` makes, and send no Nano.
