# kaleID

kaleID is a browser-extension password manager backed by ATProto identity and encrypted PDS metadata. Sites receive ordinary generated credentials; they do not receive proof of the user's DID. The extension keeps the random vault root key local and requires explicit per-origin approval before autofill.

## Workspace

- `packages/protocol` — stable collection IDs and OAuth permissions.
- `packages/core` — versioned key derivation, encrypted records, deterministic credentials, recovery, device envelopes, and backup format.
- `apps/extension` — Manifest V3 WebExtensions for Chromium, Firefox, and Safari on macOS.
- `apps/inbox` — self-hostable alias inbox with SMTP ingress and capability-scoped message access.
- `apps/recovery` — static offline backup recovery utility.
- `apps/landing` — static public landing page, privacy details and setup guide.

Node.js 24+ and npm 12+ are used for development. The inbox service uses the native `better-sqlite3` addon; if npm blocks native install scripts, explicitly approve that dependency, then rebuild it with the same Node binary that will run the service:

```sh
npm install
npm install-scripts approve better-sqlite3
npm rebuild better-sqlite3
```

## Build and checks

```sh
npm run typecheck
npm test
npm run build
npm run build:extension
npm run build:recovery
npm run build:landing
npm run build --workspace @kaleid/inbox
```

Browser targets:

```sh
npm run build:chrome --workspace @kaleid/extension
npm run build:firefox --workspace @kaleid/extension
npm run build:safari --workspace @kaleid/extension
```

Safari requires a macOS containing-app wrapper. Build one with an organization-owned bundle ID:

```sh
KALEID_SAFARI_BUNDLE_ID=es.joeinn.kaleid npm run package:safari --workspace @kaleid/extension
```

The generated Xcode project is in `apps/extension/.output/safari-app`.

Start the landing-page preview with `npm run dev:landing`. Its browser checks run with `npm run test --workspace @kaleid/landing` and use an installed Google Chrome. See [apps/landing/README.md](apps/landing/README.md) for the static hosting output and test scope.

## Self-hosting the inbox

Copy `apps/inbox/.env.example`, then configure:

- `KALEID_PUBLIC_ORIGIN=https://inbox.kaleid.joeinn.es` for the inbox API, plus a stable provider ID; the root `https://kaleid.joeinn.es` remains the static landing/OAuth origin;
- `KALEID_DOMAIN=kaleid.joeinn.es`, with MX at the root pointing to `mail.kaleid.joeinn.es`; the mail hostname must resolve directly to the SMTP ingress;
- an operator-only `KALEID_BOOTSTRAP_SECRET` of at least 32 random UTF-8 bytes;
- a stable `KALEID_INBOX_ENCRYPTION_KEY` containing 32 random bytes in canonical base64url; back it up securely because losing it makes stored messages unreadable;
- a persistent SQLite path/volume, SMTP TLS certificate and key for `mail.kaleid.joeinn.es`, and public inbound TCP 25;
- OAuth client metadata and the exact redirect URLs for the packaged browser builds.

The inbox stores messages for registered aliases; it does not forward mail or send outbound email. Message contents are AES-256-GCM encrypted at rest in SQLite, but the running service can decrypt them and the host operator controls the encryption key. Each provider account has a 100 MiB inbox cap; messages remain until deleted. Inbox messages are not included in PDS recovery backups.

The landing Worker serves `KALEID_OAUTH_CLIENT_ID=https://kaleid.joeinn.es/oauth-client-metadata.json` and the root callback; the inbox API uses a separate HTTPS origin such as `https://inbox.kaleid.joeinn.es`. The extension build uses the same client ID as `WXT_KALEID_OAUTH_CLIENT_ID`. Firefox and Chromium use `browser.identity.getRedirectURL()`; Safari uses `WXT_KALEID_SAFARI_OAUTH_REDIRECT_URI=https://kaleid.joeinn.es/oauth/callback`. Configure the exact packaged callbacks in `KALEID_OAUTH_REDIRECT_URIS`.

The root and inbox HTTPS origins may be Cloudflare-proxied; keep `mail.kaleid.joeinn.es` DNS-only because Cloudflare cannot proxy SMTP. Do not publish MX until the SMTP listener, TLS certificate, port 25, and persistent storage are ready.

## Extension build configuration

Copy `apps/extension/.env.example` to `apps/extension/.env` and set:

- `WXT_KALEID_OAUTH_CLIENT_ID=https://kaleid.joeinn.es/oauth-client-metadata.json` to match the deployed OAuth metadata URL;
- `WXT_KALEID_HANDLE_RESOLVER` to a resolver you trust. The example uses `bsky.social`, which receives the supplied handle and the client network request;
- `WXT_KALEID_SAFARI_OAUTH_REDIRECT_URI=https://kaleid.joeinn.es/oauth/callback` to the deployment callback URL.

The OAuth client metadata and callback URLs are deployment-specific. Store-issued Chromium IDs and the Firefox add-on ID affect redirect URLs and must be reflected in the deployed metadata. Firefox requires explicit data-collection consent categories and currently targets Firefox 140+. Chromium targets 119+; Safari builds target MV3 and require the macOS wrapper.

## User flow

1. Connect an ATProto identity with OAuth.
2. Create a local vault; write down and verify the 24-word recovery phrase.
3. Set up an inbox provider; each site account gets a generated email alias whose incoming messages appear in the inbox after ATProto login and vault unlock.
4. On a registration form, choose **Use my ATProto ID**, adjust the bounded password policy if needed, and create an account.
5. After the site accepts the signup credential, explicitly confirm the candidate version. Failed or abandoned candidates do not replace the active credential.
6. On another device, sign in with ATProto and have a trusted device approve the matching P-256 fingerprint. The new device then sets its own local unlock passphrase.

Credentials are never submitted automatically. Each account retains its stored versioned eTLD+1 derivation scope across Public Suffix List changes; autofill separately requires approval for the exact scheme/host/port origin. The page can read values after they are filled into its DOM.

## Compatibility

Data from earlier builds is not backward compatible with this release: storage keys, cryptographic domain-separation strings, the PDS collection namespace, and backup format identifier changed. This build cannot open earlier local vault state, encrypted records, or backups; it also derives different deterministic passwords and email aliases from the same inputs. Earlier forwarding-provider setup, routes, and destinations are not migrated into inbox aliases. The old database path is not automatically opened or imported; it remains untouched. Previous messages were not stored by the forwarding service. No migration is provided. Keep an accessible copy of the earlier app and data if you need those values.

## Offline recovery

Build the recovery utility with `npm run build:recovery`. Serve `apps/recovery/dist` from a local static server on `localhost`, open it, then disconnect from the network. It performs no runtime network requests. It needs the recovery mnemonic and the versioned encrypted vault backup JSON; the file contains encrypted PDS records, not plaintext site policies or passwords.

## Deployment prerequisites and limits

- Production collections use `es.joeinn.kaleid.vault.policy` and `es.joeinn.kaleid.vault.device`, under the reverse-domain namespace of `kaleid.joeinn.es`. Treat these NSIDs as stable once user data is published.
- Production inbox delivery requires HTTPS OAuth metadata/redirects, MX pointing to inbound SMTP on port 25, a valid SMTP TLS certificate, persistent SQLite storage, and operator-issued bootstrap credentials.
- Inbox content is encrypted at rest with a Coolify-held key; routing metadata stays plaintext and the running service can decrypt messages. Inbox messages are not part of PDS backups. No outbound relay or external mailbox forwarding is used.
- PDS deletion/rollback is not provably detectable by a fresh device. Local caches and exported encrypted backups provide best-effort protection only.
- kaleID does not provide a post-quantum security guarantee. ATProto OAuth/TLS and conventional sites are external protocols outside kaleID's cryptographic control.
