Cookbooks syncs

When an operator syncs a cookbook to a device, the backend publishes a
request over MQTT. Instead of embedding every recipe template in the message, things5
sends a link to a manifest you download over HTTPS, so the MQTT
payload stays small no matter how many recipes the cookbook contains.

1. Request (backend → device)

Topic:
things5/v1/devices/<DEVICE_ID>/cmd/sync_recipe_templates_v2_req

Payload:

{
  "request_id": "<uuid>",
  "cookbook_sync_log_id": "<uuid>",
  "manifest_url": "https://...signed-url...",
  "expires_at": 1779711048527
}

- manifest_url — pre-signed HTTPS URL to the manifest. Valid until
expires_at (Unix epoch, milliseconds). Download it before then.
- request_id / cookbook_sync_log_id — echo both back in your response.

2. Manifest (device downloads from manifest_url)

{
  "manifest_version": 1,
  "cookbook_sync_id": "<uuid>",
  "recipes": [
    {
      "index": 0,
      "id": "<uuid>",
      "hash": "<sha256-hex>",
      "hash_version": 1,
      "url": "https://.../recipes/0.json"
    }
  ]
}

For each entry, compare hash against the recipe you already have stored
locally:  
- Same hash → you already have this recipe, skip the download.
- Different/missing hash, or hash_version you don't support →
download url.

The hash is a content fingerprint (algorithm versioned by hash_version).
This lets you re-sync a cookbook and only fetch what actually changed.

3. Recipe (device downloads from each url)

{
  "id": "<uuid>",
  "index": 0,
  "name": "...",
  "description": "...",
  "machine_model_id": "<uuid>",
  "phases": [ ... ],
  "metadata": [ ... ],
  "hash": "<sha256-hex>",
  "hash_version": 1 
}

Apply it and free the buffer before fetching the next one (you don't need
to hold the whole cookbook in memory).

4. Response (device → backend)

Topic:
things5/v1/devices/<machine_id>/cmd/sync_recipe_templates_res

Payload:
{
  "request_id": "<uuid>",
  "cookbook_sync_log_id": "<uuid>",
  "synced_at": 1779711048527,
  "errors": [] 
}

- Echo request_id and cookbook_sync_log_id from the request.
- synced_at — Unix epoch (ms) when the sync completed.
- errors — empty array on success, otherwise one string per failure.

▎ The response is identical to v1, so the backend correlates v1 and v2
▎ the same way. A device that supports v2 subscribes to the
▎ ..._v2_req topic; legacy devices keep using ..._req.