use clap::{Args, Parser, Subcommand, builder::PossibleValue};
use uuid::Uuid;
const AFTER_HELP: &str = "\
Start:
hook accounts --json what signing in needs (works offline)
hook login Carbons: approve a code in the browser
silicon-accounts login --app hook -q | hook login --slt-stdin
Silicons: sign in with a short-lived token
hook login status --json who you are signed in as
Then:
hook create GitHub a hook for the signed-in Silicon
hook --silicon si:scout list a Silicon you look after
hook events verified requests, newest first
Explore: hook commands · hook <command> --help · hook docs <topic>
Docs: https://docs.hook.teamofsilicons.com · Source: https://github.com/teamofsilicons/silicon-hook
Rust: https://crates.io/crates/silicon-hook-client · Bugs: hook report --help";
#[derive(Debug, Parser)]
#[command(
name = "hook",
version,
about = "Signed provider webhooks for Silicons: create the URLs providers call, verify every request, read the history.",
long_about = "Silicon Hook gives each Silicon its own signed webhook URLs. Providers (GitHub, Stripe, ...) post to them; Hook verifies each request against the hook's signature policy, keeps verified requests for 14 days and withheld ones in a separate log, and (when the server delivers through Ting) passes a reference to the Silicon.\n\nA Silicon's hooks belong to it. The Silicon and its custodian (the Carbon who looks after it) can do everything; they can give other Carbons and Silicons view or manage access.\n\nSign in with Silicon Accounts: Carbons run `hook login`; Silicons pipe a short-lived token from `silicon-accounts login --app hook -q` into `hook login --slt-stdin`. Results print as JSON on stdout; hints go to stderr (silenced by --json).",
after_help = AFTER_HELP
)]
pub struct Cli {
#[arg(
long,
global = true,
default_value = "default",
help = "Saved profile: its own sign-in and settings, in the same state directory"
)]
pub profile: String,
#[arg(
long,
global = true,
env = "SILICON_HOOK_URL",
help = "Hook API origin [default: https://api.hook.teamofsilicons.com]"
)]
pub url: Option<String>,
#[arg(
long,
global = true,
env = "ACCOUNTS_URL",
help = "Silicon Accounts origin [default: https://accounts.teamofsilicons.com]"
)]
pub accounts_url: Option<String>,
#[arg(
long,
global = true,
value_name = "SI:ID|UUID",
help = "The Silicon to act on; defaults to `hook config set silicon`, else the signed-in Silicon"
)]
pub silicon: Option<String>,
#[arg(
long,
global = true,
help = "JSON only: no hints on stderr; errors as JSON on stderr"
)]
pub json: bool,
#[arg(
long,
global = true,
help = "Reuse this key when retrying the same change after an uncertain result"
)]
pub idempotency_key: Option<String>,
/// Removed in 1.0; kept only to explain the change.
#[arg(long, global = true, hide = true)]
pub org: Option<String>,
/// Removed in 1.0; kept only to explain the change.
#[arg(long, global = true, hide = true)]
pub test: Option<String>,
/// Removed in 1.0; kept only to explain the change.
#[arg(long, global = true, hide = true)]
pub production: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Sign in with Silicon Accounts (Carbons: device code; Silicons: short-lived token), or check the sign-in with `login status`.
Login(Login),
/// Sign out: revoke this profile's sign-in at Silicon Accounts and forget it locally.
Logout,
/// Show the saved sign-in without contacting any service.
Whoami,
/// What signing in to Hook needs: app id, Silicon Accounts URL, API URL, version. Works offline, signed out.
Accounts,
#[command(hide = true)]
Iam,
/// List the Silicons you can open: yourself, the Silicons you look after, and the ones shared with you.
Silicons,
/// Create a provider webhook for the Silicon. Signing is on by default; the generated secret prints once.
#[command(
after_help = "Examples:\n hook create GitHub --signature @github-policy.json\n hook create Stripe --secret-file stripe-secret.txt\n hook create LocalDemo --unsigned\nThe URL to give the provider is endpoint_url. See `hook docs signatures` for policies."
)]
Create {
/// Provider name, used in event summaries (e.g. GitHub).
name: String,
#[arg(long, help = "Free text")]
description: Option<String>,
#[arg(long, help = "IANA time zone for event summaries [default: UTC]")]
time_zone: Option<String>,
#[arg(
long,
help = "Signature policy as JSON or @file; see hook docs signatures"
)]
signature: Option<String>,
#[arg(
long,
help = "Bring your own secret from a file ('-' reads stdin); spaces kept, one trailing line ending removed"
)]
secret_file: Option<String>,
#[arg(long, help = "Accept unsigned requests (no verification)")]
unsigned: bool,
},
/// Set or replace a hook's secret (bring your own secret); the URL and policy stay.
SetSecret {
id: Uuid,
#[arg(
long,
help = "Secret file; '-' reads stdin. Spaces kept; one trailing line ending removed"
)]
secret_file: String,
#[arg(long, value_parser = ["utf8", "ascii", "hex", "base64", "base64url", "raw"])]
secret_encoding: Option<String>,
},
/// List the Silicon's hooks: provider URL, state, last request.
List {
#[arg(long, help = "Also show deleted hooks that can still be restored")]
include_deleted: bool,
},
/// Show one hook's settings and signing policy (never its secret).
Show { id: Uuid },
/// Change a hook with a JSON patch: name, description, time_zone, enabled, signature.
Update {
id: Uuid,
#[arg(
long,
help = "JSON or @file, e.g. '{\"description\":null}' clears the description"
)]
patch: String,
},
/// Delete a hook; restore it within 45 days.
Delete { id: Uuid },
/// Restore a deleted hook with its URL and secret.
Restore { id: Uuid },
/// Resume ingress for one or more hooks (all or nothing).
Enable {
#[arg(required = true)]
ids: Vec<Uuid>,
},
/// Pause ingress for one or more hooks; URLs and history stay.
Disable {
#[arg(required = true)]
ids: Vec<Uuid>,
},
/// Rotate a hook's URL (the old one is retired for good) or its signing secret.
Rotate {
#[command(subcommand)]
kind: Rotate,
},
/// Read verified requests, newest first; --hook for one hook, else the whole Silicon.
#[command(
after_help = "Examples:\n hook events --limit 20\n hook events --hook <id> --limit 5\n hook events --cursor <next_cursor> the next page\n hook event <event-id> one event in full"
)]
Events(History),
/// Read withheld (unverified) requests, kept 14 days.
Blocked(History),
/// Fetch one retained event with its original provider request.
Event { id: Uuid },
/// Where one event's delivery through Ting stands.
Publication { event_id: Uuid },
/// Who can see and manage the Silicon's hooks: list, grant, revoke, leave.
#[command(
after_help = "Examples:\n hook --silicon si:scout access list\n hook --silicon si:scout access grant c:ada --level view\n hook --silicon si:scout access grant si:helper --level manage\n hook --silicon si:scout access revoke c:ada\n hook --silicon si:scout access leave (a grantee gives up its own access)\nThe Silicon and its custodian always have full access. A Silicon looked after by another custodian accepts a grant only after it allowed you: see hook allow-list --help."
)]
Access {
#[command(subcommand)]
action: Access,
},
/// Accounts the Silicon accepts access grants from although their custodian is not its custodian.
#[command(
after_help = "Run by the Silicon (or its custodian) that will receive access:\n hook --silicon si:helper allow-list add c:ada (c:ada and the Silicons it looks after may now grant si:helper access)\n hook --silicon si:helper allow-list list\n hook --silicon si:helper allow-list remove c:ada"
)]
AllowList {
#[command(subcommand)]
action: AllowList,
},
/// Receive the Silicon's own Silicon Accounts events (sign-outs, id changes...) in a hook.
#[command(
after_help = "Flow:\n 1. hook connect-accounts prints the hook URL and the silicon-accounts command\n 2. silicon-accounts webhook set <url> (the Silicon) prints a whsec_ secret once\n silicon-accounts silicon webhook set <si:id> <url> (its custodian)\n 3. hook connect-accounts --secret-file - stores that secret on the hook\nUntil the secret is stored, deliveries are withheld as unverified."
)]
ConnectAccounts {
#[arg(
long,
help = "Store the whsec_ secret Silicon Accounts printed ('-' reads stdin)"
)]
secret_file: Option<String>,
},
/// Delivery through Ting: enrol yourself, and (Carbons) subscribe to a Silicon's events.
#[command(
after_help = "Examples:\n hook system delivery is delivery through Ting on for this Hook?\n hook receiving register enrol yourself with Ting\n hook --silicon si:scout receiving subscribe (a Carbon with access) copies of future events\nWhen the server has no Ting these answer delivery_disabled; read events with hook events."
)]
Receiving {
#[command(subcommand)]
action: Receiving,
},
/// Read or change local settings: URLs, default Silicon, home directory.
#[command(
after_help = "Examples:\n hook config show\n hook config set silicon si:scout\n hook config set url http://127.0.0.1:4201 && hook config set accounts-url http://localhost:9590\n hook --profile lab login a second sign-in in the same home"
)]
Config {
#[command(subcommand)]
action: Config,
},
/// Check the Hook API: version, readiness, delivery.
System {
#[command(subcommand)]
action: System,
},
/// List every command path; --json adds each command's full help. No sign-in needed.
Commands,
/// Read the bundled guides offline: overview, signin, cli, client, receiving, signatures, delivery, contracts, configuration, deployment, releases.
Docs {
#[arg(default_value = "overview")]
topic: String,
},
/// File a bug report on GitHub with your authenticated gh CLI (only your text and the version are sent).
Report {
message: String,
#[arg(long, help = "A silicon-hook pull request with a fix")]
pr: Option<String>,
},
/// Show the source repository, docs, packages and version.
About,
}
#[derive(Debug, Args)]
#[command(
args_conflicts_with_subcommands = true,
subcommand_negates_reqs = true,
after_help = "Carbons: hook login prints a code and a URL; approve it in a browser (10 minutes)\nSilicons: silicon-accounts login --app hook -q | hook login --slt-stdin\n hook login <SLT> and --slt <SLT> also work (the token is single use, two minutes)\nThen: hook login status --json"
)]
pub struct Login {
#[command(subcommand)]
pub action: Option<LoginAction>,
#[arg(
value_name = "SLT",
conflicts_with_all = ["slt", "slt_stdin", "slt_file"],
help = "Short-lived token from `silicon-accounts login --app hook -q` (prefer --slt-stdin)"
)]
pub token: Option<String>,
#[arg(long, conflicts_with_all = ["slt_stdin", "slt_file"], help = "Short-lived token; visible to other processes, prefer --slt-stdin")]
pub slt: Option<String>,
#[arg(
long,
conflicts_with = "slt_file",
help = "Read the short-lived token from stdin"
)]
pub slt_stdin: bool,
#[arg(
long,
hide = true,
help = "File holding the short-lived token; '-' reads stdin"
)]
pub slt_file: Option<String>,
#[arg(
long,
help = "Device sign-in: also open the approval page in a browser"
)]
pub open: bool,
#[arg(long, help = "Device sign-in: name this machine on the approval page")]
pub label: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum LoginAction {
/// Who this profile is signed in as. Refreshes the sign-in when needed and asks Hook to confirm it; --offline reads only the saved file.
#[command(
after_help = "With --json the exit code is always 0: {\"authenticated\":false} when signed out; when signed in: uuid, id, kind, display_name, expires_at, refresh_expires_at, verified. Without --json it exits 1 when signed out."
)]
Status {
#[arg(long, help = "Read only the saved sign-in; contact no service")]
offline: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum Rotate {
/// Retire the hook's URL for good and issue a new one.
Endpoint { id: Uuid },
/// Generate a new signing secret (prints once); the old one stops verifying.
Secret { id: Uuid },
}
#[derive(Debug, Args)]
pub struct History {
#[arg(long, help = "Only this hook")]
pub hook: Option<Uuid>,
#[arg(long, default_value_t = 100, help = "How many, 1 to 10000")]
pub limit: u32,
#[arg(long, help = "next_cursor from the previous page")]
pub cursor: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum Access {
/// Who has access to the Silicon's hooks, and your own access.
List,
/// Give a Carbon or Silicon view or manage access (the Silicon or its custodian only).
Grant {
#[arg(
value_name = "C:ID|SI:ID|UUID",
help = "The Carbon or Silicon to give access to"
)]
account: String,
#[arg(long, value_parser = ["view", "manage"], help = "view: read hooks and history; manage: also create and change hooks")]
level: String,
},
/// Remove an account's access (the Silicon or its custodian only).
Revoke {
#[arg(value_name = "C:ID|SI:ID|UUID", help = "The account whose access ends")]
account: String,
},
/// Give up your own access to the Silicon.
Leave,
}
#[derive(Debug, Subcommand)]
pub enum AllowList {
/// The accounts on the Silicon's allow-list.
List,
/// Allow an account (and its Silicons' grants) to give this Silicon access.
Add {
#[arg(value_name = "C:ID|SI:ID|UUID", help = "The account to allow")]
account: String,
},
/// Remove an account; grants it already gave stay until revoked.
Remove {
#[arg(value_name = "C:ID|SI:ID|UUID", help = "The account to remove")]
account: String,
},
}
#[derive(Debug, Subcommand)]
pub enum Receiving {
/// Enrol yourself with Ting to receive Hook's notifications (when the server delivers through Ting).
Register,
/// Your (a Carbon's) subscription to the Silicon's events.
Status,
/// Receive copies of the Silicon's future events (Carbons with access).
Subscribe,
/// Stop receiving copies of the Silicon's events.
Unsubscribe,
}
#[derive(Debug, Subcommand)]
pub enum Config {
/// Show the settings in effect and where each comes from; tokens are never shown.
Show,
/// List the saved profiles.
Profiles,
/// Keep state under `<location>/.silicon-hook` from now on (the directory must exist).
Home { location: String },
/// Set url, accounts-url or silicon for the profile.
Set {
// `telemetry` is still accepted (hidden) so scripts that turned it off
// keep working; Hook records no telemetry, so it changes nothing.
#[arg(value_parser = [
PossibleValue::new("url"),
PossibleValue::new("accounts-url"),
PossibleValue::new("silicon"),
PossibleValue::new("telemetry").hide(true),
])]
key: String,
value: String,
},
/// Go back to the default for url, accounts-url or silicon.
Unset {
#[arg(value_parser = ["url", "accounts-url", "silicon"])]
key: String,
},
}
#[derive(Debug, Subcommand)]
pub enum System {
/// The Hook API's version and the API major it serves (no sign-in).
Version,
/// Whether the Hook API is ready (no sign-in).
Health,
/// Whether this Hook delivers events through Ting (needs sign-in).
Delivery,
}