Builds on the seed-URL pairing in #21 (stacked).
(b) TTL — PairMachineData.duration_hours (validated > 0) threads through
pair_spire -> create_new_token (lnbits#55). None = non-expiring.
(c) Revoke — POST /machines/{id}/revoke -> revoke_spire ->
admin_client.revoke_key_user(spire-<id>). Per spirekeeper#22, revoke
MUST go through KeyUser.revokedAt (revoke_key_user), NOT token revoke:
lnbits eager-binds (redeems) the connect token at provision, so
nsecbunkerd has materialised the policy into per-KeyUser grants its
ACL checks BEFORE the Token.revokedAt filter -> token revoke is a
silent no-op. Returns RevokeResult{revoked_count}: >=1 = cut, 0 =
never bound. set_machine_unpaired clears paired_at (keeps npub +
bunker_spire_key_name for audit / re-pair).
7 new tests (duration threading + default-None; revoke routes to
revoke_key_user and never token-revoke + error mapping; endpoint wiring
revoke happy/zero/502). 210 green; new code black/ruff-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>