# Lock Recipe (demo)

Canonical write-up for `lock.html` / `lock.ts`. Generic write-command pattern for BLE smart locks. **The UUIDs shown are demo-only; no production handshake is performed.**

## Preconditions

- **iOS 15.0+** Safari with the WebBLE Safari Web Extension installed.
- Extension enabled under **Settings → Safari → Extensions → WebBLE → Allow**.
- Per-site permission set to **"Always Allow on Every Website"**.
- The real lock would typically require an iOS-level pairing bond for encrypted characteristics.

## Spec citation

- **No SIG-assigned Lock service exists.** There is no assigned UUID for "smart lock" at [Bluetooth SIG Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/) (checked against Document 1.5 *Service UUIDs*).
- Real vendors publish their own integration specs. Examples of **publicly documented** lock BLE protocols:
  - **Nuki Smart Lock BLE API** — published at `https://developer.nuki.io/` (v2.2.1 as of authoring). Uses a pairing command set with ECDH-curve25519 key exchange + HMAC-SHA-256 per-command authentication.
  - **August Lock** — proprietary, partner-only; not public.
  - **Yale Linus** — proprietary; only accessible through the Nest / Yale Access SDK.
- This recipe therefore uses a **labelled demo service** and demonstrates only the underlying W3C Web Bluetooth call pattern:
  - `BluetoothRemoteGATTCharacteristic.writeValue(buffer)` — W3C Web Bluetooth CG Spec §3.4.5.
  - `BluetoothRemoteGATTCharacteristic.writeValueWithResponse(buffer)` — §3.4.5 (split API in modern spec revisions).

## Exact `requestDevice` filter

```js
{ filters: [{ services: ['0000fee0-0000-1000-8000-00805f9b34fb'] }] }
```

The UUID prefix `fee0` falls in the 16-bit alias range reserved by the SIG for member companies; `0x0000FEE0` is assigned to Anhui Huami and widely used as a **demo value in BLE documentation** (it is explicitly not a Lock service). Change to the UUID your vendor publishes.

## GATT sequence

1. `navigator.bluetooth.requestDevice({ filters: [{ services: [LOCK_SERVICE] }] })` — user picker.
2. `device.gatt.connect()`.
3. `server.getPrimaryService(LOCK_SERVICE)`.
4. `service.getCharacteristic(LOCK_COMMAND)`.
5. `char.writeValue(new Uint8Array([OPCODE, ...payload]))`.

**Production additions (not shown):**

- Exchange keys at pairing time and store them in `IndexedDB` or the native keychain.
- Encrypt the payload and append an HMAC per the vendor spec.
- Include a monotonically increasing nonce; reject commands with duplicate or out-of-order nonces.
- Await a status notification on the lock's status characteristic before considering the command confirmed.

## Expected output

```
unlock command written to My Demo Lock
```

No confirmation is attempted in the demo — a real lock would notify on a status characteristic.

## Common errors

| Error (`name`) | Cause | Remediation |
|---|---|---|
| `NotFoundError` | User cancelled picker, lock out of range, or service UUID mismatch. | Confirm the lock is advertising (most sleep deeply); verify UUIDs against the vendor spec. |
| `SecurityError` on `getPrimaryService` | Characteristic is encrypted, iOS bond missing. | Pair through iOS Settings → Bluetooth first. |
| `NetworkError` | Connection dropped mid-write (common with battery-powered locks). | Reconnect; retry with a fresh nonce. **Never replay the same encrypted payload.** |
| `InvalidStateError` on `writeValue` | GATT disconnected between `connect()` and `writeValue()`. | Call `gatt.connect()` again before retrying. |

## Verbatim HTML snippet

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Lock (demo) — Web Bluetooth on iPhone</title>
</head>
<body>
  <aside data-agent-notes>
    <strong>Preconditions</strong>
    <ul>
      <li>iOS 15.0+ Safari with the WebBLE extension installed.</li>
      <li>Extension enabled: Settings → Safari → Extensions → WebBLE → Allow.</li>
      <li>Per-site permission: "Always Allow on Every Website".</li>
    </ul>
    <strong>Spec</strong>: No SIG-assigned Lock service exists. The UUIDs below (<code>0000fee0-…</code> / <code>0000fee1-…</code>) are labelled <strong>demo custom service</strong>. Real smart-lock products (e.g. Nuki, August, Yale) ship proprietary authenticated handshakes — consult the vendor integration spec. Generic pattern per W3C Web Bluetooth CG spec §3.4.5 <em>writeValue()</em>.
    <p>Canonical write-up: <a href="./lock.md">lock.md</a>.</p>
  </aside>

  <h1>Lock — generic write-command pattern (demo)</h1>
  <button id="start">Pair lock & send unlock</button>
  <pre id="log" aria-live="polite"></pre>

  <script type="module">
    import '@ios-web-bluetooth/core/auto';

    // Demo-only UUIDs — clearly labelled. Replace with your vendor's UUIDs.
    const LOCK_SERVICE = '0000fee0-0000-1000-8000-00805f9b34fb';
    const LOCK_COMMAND = '0000fee1-0000-1000-8000-00805f9b34fb';

    const log = (line) => {
      const pre = document.getElementById('log');
      pre.textContent += line + '\n';
    };

    // Placeholder: in a real integration, the authToken is negotiated at
    // pairing time via the vendor handshake. DO NOT ship a hard-coded token.
    const authToken = new Uint8Array([0x00, 0x00, 0x00, 0x00]);

    document.getElementById('start').addEventListener('click', async () => {
      try {
        const device = await navigator.bluetooth.requestDevice({
          filters: [{ services: [LOCK_SERVICE] }]
        });
        const server = await device.gatt.connect();
        const service = await server.getPrimaryService(LOCK_SERVICE);
        const char = await service.getCharacteristic(LOCK_COMMAND);

        // Example: 0x02 = unlock, followed by an auth token negotiated at pairing
        await char.writeValue(new Uint8Array([0x02, ...authToken]));
        log(`unlock command written to ${device.name ?? device.id}`);
      } catch (err) {
        log(`error: ${err.name}: ${err.message}`);
      }
    });
  </script>
</body>
</html>
```

## Canonical JS snippet (byte-identical to `docs-src/recipes.md`)

```js
const LOCK_SERVICE = '0000fee0-0000-1000-8000-00805f9b34fb';
const LOCK_COMMAND = '0000fee1-0000-1000-8000-00805f9b34fb';

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: [LOCK_SERVICE] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(LOCK_SERVICE);
const char = await service.getCharacteristic(LOCK_COMMAND);

// Example: 0x02 = unlock, followed by an auth token negotiated at pairing
await char.writeValue(new Uint8Array([0x02, ...authToken]));
```

Real locks require vendor authentication handshakes — consult your lock's integration spec.
