# Locker — passkey sign-in + state storage for *.gmbuell.com apps

Locker gives any static app served from an `https://*.gmbuell.com` origin
one-click passkey sign-in and a per-user, per-app JSON state blob. No
backend, no build step, no configuration, no API keys: import the client
from this service and call it.

## Quickstart

```html
<div id="signin"></div>
<script type="module">
  import { createLocker } from "https://locker.gmbuell.com/client.js";

  const locker = createLocker();
  locker.mountButton(document.querySelector("#signin"), {
    onReady: async () => {
      const state = (await locker.load()) ?? { visits: 0 }; // null until first save
      state.visits++;
      await locker.save(state);
    },
  });
</script>
```

`mountButton` renders a "Sign in with passkey" button and fires `onReady`
once signed in — immediately and without a click when a session already
exists, so returning users never see a prompt.

## API

Zero-dependency ES module. All methods return promises and throw
`LockerError` (with `.status`, the HTTP code) on failure.

- `createLocker()` → a locker instance. No arguments.
- `locker.signIn()` — fully automatic: resolves silently if a session
  exists, else does a passkey login, else registers a new passkey and
  account. For custom UI instead of `mountButton`; call it from a click
  handler (browsers require a user gesture for passkey prompts).
- `locker.load()` → this app's state, or `null` if nothing saved yet.
- `locker.save(state)` — replace this app's state. Any JSON value up to
  256 KiB; last write wins. `save(null)` clears; `save(undefined)` throws.
- `locker.signOut()` — end the session (all `*.gmbuell.com` apps).
- `locker.signedIn` → boolean, synchronous.

## Rules

- Your app must be served from `https://<app>.gmbuell.com`. Every other
  origin is rejected — there is nothing to configure, and no way to allow
  localhost.
- State is namespaced by your app's hostname: each `*.gmbuell.com`
  subdomain gets its own blob per user. You get exactly one blob — make it
  a single JSON object.
- The passkey is the account. Sign-in is shared across all
  `*.gmbuell.com` apps (one session cookie on `locker.gmbuell.com`);
  there are no usernames, emails, or passwords anywhere.
- Evergreen browsers only (uses the `PublicKeyCredential` JSON statics).

Working example (view source): <https://locker.gmbuell.com/>
