# 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".

Package: `@nano133/signin`. It works with any Node.js server and any
database that can insert a row with a unique key. This spec uses one way:
the person's wallet signs a short message, and your server checks the
signature. No XNO moves, and your server holds no key.

## 0. Decide (ask)

1. The site's domain, exactly as users see it (the message names it, so a
   signature for your site counts nowhere else).
2. Where your sessions live (your own session code, or your framework's).

## 1. Install

```sh
npm install @nano133/signin
```

## 2. The once-only store

A signature counts one time. Give the package a `useOnce(key, ttlMs)` that
inserts a row with a unique key and answers false when the row exists:

- Firestore: `firestoreOnce(db, collection)` from `@nano133/signin/firestore`,
  and a TTL policy on that collection's `deleteAt`.
- Other databases: a table with a unique key and an expiry.
- Tests only: `memoryOnce()` from `@nano133/signin/memory`.

**Check:** the same key twice: the second answers false.

## 3. The message

The browser builds `signinMessage(domain, address, Date.now())` (from
`@nano133/signin/message`), the wallet signs it with the account's key, and
the browser posts `{ address, at, signature }`.

## 4. The signature route

- `POST /signin/signature`: `checkSignatureSignin({ domain, useOnce }, body)`
  from `@nano133/signin/signature` → create the session for the returned
  address. Map `SigninError` status to the answer. **Required:** rate-limit
  it per visitor.
- When you create the session: give it a new session id (never reuse one from
  before the sign-in), and show "Signed in as nano_…" with the account
  (required).

**Check:** the same signature twice: the second is refused; a signature for
another domain is refused; one older than 5 minutes is refused. An address
must be whole (its shape and its checksum): the package refuses one that is
not. Never make an account from an address that it did not return.

## 5. Test

1. A site test with `memoryOnce()` and a key made in the test.
2. One real sign-in with the owner's wallet.

## Done when

- [ ] The signature route is rate-limited (required).
- [ ] A new session id at each sign-in.
- [ ] The screen shows "Signed in as nano_…" after every sign-in.
- [ ] The message uses the site's real domain.
- [ ] The once-only store refuses a key it has seen; it has a TTL.
- [ ] Keys and secrets are in a secret store, never logged.
