silicon-hook-cli 1.1.0

The hook CLI: manage a Silicon's signed provider webhooks, signed in with Silicon Accounts
# Delivery through Ting

Hook receives, verifies and keeps provider webhooks. Ting carries each verified
event to the Silicon (and to Carbons who subscribed). The app that hosts the
Silicon handles receiving; nobody configures Hook or Ting by hand.

Delivery is optional. Without it Hook still receives, verifies and stores every
event for 14 days, and Silicons read them with `hook events` or the history API.

## Turning delivery on or off

| Variable | Meaning |
| --- | --- |
| `HOOK_TING_URL` | Ting's origin. Unset: delivery is off. Plain `http` only for loopback. |
| `HOOK_TING_APP_ID` | Ting's app id at Silicon Accounts: every proof Hook presents to Ting is issued for this app (default `ting`; never Hook's own id). |
| `HOOK_TING_TIMEOUT_SECONDS` | Deadline for one call to Ting, 1 to 15 seconds (default 10). |
| `HOOK_TING_POLL_MILLISECONDS` | How often the publisher looks for queued sends, 100 to 30000 ms (default 1000). |

With `HOOK_TING_URL` unset, Hook queues nothing (so turning delivery on later
does not flood Silicons with old events), logs that delivery is off at startup,
and says so everywhere a client can look: `/readyz` (`delivery.ting: disabled`),
`GET /api/v3/delivery`, `409 delivery_disabled` from enrolment and
subscriptions, and `state: delivery_disabled` in publication status.

Before setting `HOOK_TING_URL`, Ting must accept Silicon Accounts proofs from
Hook (Ting's own migration is outside Hook), and Ting's operator must register
the `hook.webhook.received` type. Until then keep it unset.

## How Hook talks to Ting

Hook signs every call to Ting with a Silicon Accounts proof made with Hook's own
app credentials:

| Call | Proof | Scope |
| --- | --- | --- |
| Send a notification, read its receipt | App verification (no user) | `tings.send`, `sent.query` |
| Enrol a recipient | User verification, from the recipient's own Hook access token | `tings.subscribe` |

App verification proofs are kept in memory and renewed one at a time; a User
verification proof is made for each enrolment while the recipient's request is
live. Hook stores no delegated tokens. Recipients are addressed by their Silicon
Accounts uuid and current id, `{"uuid", "id"}`.

When Silicon Accounts refuses or cannot answer a proof request (for example
because `HOOK_TING_APP_ID` names no app yet), Hook logs why once and waits before
asking again: 30 seconds, doubling up to 5 minutes while it keeps failing. Queued
sends stay queued meanwhile, with publication state `pending` and
`last_error_code: proof_unavailable`, so a long queue never becomes one proof
request per send.

## Event and acknowledgment flow

1. Hook verifies the provider's signature, then stores the original request and
   the exact outgoing Ting request in one transaction. The provider hears
   `webhook.ok` only after the commit.
2. The publisher sends each queued notification with a fresh proof, retrying with
   the same bytes and the same producer key
   (`hook:{event id}:{sha256(recipient uuid)}`) until Ting confirms it stored the
   send. An uncertain result never creates a second notification.
3. Ting receives the compact `new_event` envelope: the provider name and a
   reference (event id, `silicon: {uuid, id}`, hook id, original sequence and
   time, and a summary with the provider and receipt time in the hook's IANA time
   zone). Provider bodies and headers stay in Hook.
4. The receiving app fetches `GET /api/v3/silicons/{silicon uuid}/events/{event id}`
   with the recipient's own Hook token. Failed access checks and expired events
   must not become application work.
5. The app durably accepts and deduplicates the event before acknowledging Ting's
   local batch. Ting may replay or reorder; deduplicate on the event id.

Hook keeps events 14 days, even if Ting keeps the reference longer.

## Recipients

- **The Silicon** receives its own events. Its sends use Ting's required
  (automation) mode, which the Silicon opts into through its own app; until then
  they stay pending with `required_delivery_not_enabled`. Hook never downgrades
  the mode.
- **Carbons** with `view` (or more) on a Silicon, and its custodian, can subscribe
  to copies of its future events: `POST /api/v3/silicons/{silicon}/delivery/subscription`
  (`hook receiving subscribe`). Copies use ordinary delivery and follow the
  Carbon's notification preferences. Hook checks the Carbon's access again before
  every send, and drops the subscription when access ends (grant revoked,
  custodian changed, Hook's access removed, account deleted). At most 100
  observers per Silicon.

Enrolment (`POST /api/v3/delivery/recipient`, `hook receiving register`) grants
Hook's notifications to the caller in Ting. Attaching a destination is done in
Ting by the receiving app; Hook never stores the receiving URL.

## Publication status

`GET /api/v3/silicons/{silicon}/events/{event}/publication` reports
`delivery_disabled`, `not_queued`, `pending` (with `last_error_code`),
`accepted_by_ting`, `accepted_silently` (ordinary send while muted) or
`not_delivered_legacy`, plus `delivery`, `silent`, attempts and, when available,
Ting's receipt with per-destination delivery and read acknowledgments. A receipt
means a destination accepted the event, never that the Silicon finished its
work.

## From Hook before 1.0

Sends queued before 1.0 that Ting never accepted cannot be signed with Silicon
Accounts proofs; migration `0019` marks them `legacy_identity` and publication
shows `not_delivered_legacy` until the event expires. Earlier Carbon
subscriptions are not carried over: Carbons subscribe again. The publisher setup
and the separate Ting approval of earlier versions are gone.