1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
use clap::{Args, Parser, Subcommand};
use uuid::Uuid;
#[derive(Debug, Parser)]
#[command(
name = "hook",
version,
about = "Signed webhooks for Silicons, with reliable local delivery.",
long_about = "Manage Silicon Hook through its official Rust client. Sign in with an IAM short-lived token, then configure your delivery URL with hook webhook. Use the same commands in a sandbox with --test <environment-id>.",
after_help = "Start: hook iam --json; hook login <slt> --org tos\nDelivery: hook webhook http://127.0.0.1:9000/events\nThen: hook --silicon cos:tos create GitHub\nExplore: hook commands; hook <command> --help\nDocs: https://docs.hook.teamofsilicons.com · Source: https://github.com/teamofsilicons/silicon-hook\nRust: https://crates.io/crates/silicon-hook-client · Bugs: hook report --help"
)]
pub struct Cli {
#[arg(
long,
global = true,
default_value = "default",
help = "Independent local identity/profile"
)]
pub profile: String,
#[arg(
long,
global = true,
env = "SILICON_HOOK_URL",
help = "Backend origin; defaults to backend.hook.teamofsilicons.com"
)]
pub url: Option<String>,
#[arg(
long,
global = true,
env = "SILICON_HOOK_ORG",
help = "Organization handle in the selected production/test plane"
)]
pub org: Option<String>,
#[arg(
long,
global = true,
help = "Target Silicon, e.g. cos:tos; defaults to the signed-in Silicon"
)]
pub silicon: Option<String>,
#[arg(
long,
global = true,
help = "Test environment UUID; its root key is stored separately"
)]
pub test: Option<Uuid>,
#[arg(
long,
global = true,
conflicts_with = "test",
help = "Use the separate production session for this command"
)]
pub production: bool,
#[arg(
long,
global = true,
env = "ISI",
help = "Optional internal Silicon identifier stored in local delivery metadata"
)]
pub isi: Option<String>,
#[arg(
long,
global = true,
help = "Structured JSON output without next-step prose"
)]
pub json: bool,
#[arg(
long,
global = true,
help = "Reuse this key when retrying the same mutation"
)]
pub idempotency_key: Option<String>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Submit a bug report to GitHub using an already authenticated gh CLI. No logs or credentials are collected.
Report {
message: String,
#[arg(
long,
help = "Optional silicon-hook GitHub pull request URL containing a fix"
)]
pr: Option<String>,
},
/// Show source, online docs and published package links.
About,
/// Sign in with an IAM short-lived token, or check login status.
Login(Login),
/// Discover the IAM app_id needed to request a short-lived token; no login needed.
Iam,
/// Configure or replace this identity's local delivery URL and start its relay.
Webhook {
webhook_url: String,
#[arg(long, help = "Optional local delivery HMAC key file; '-' reads stdin")]
secret_file: Option<String>,
#[arg(
long,
help = "Explicitly mark a remote webhook as a sandbox-only destination"
)]
test_destination: bool,
},
/// Detach this identity's local delivery URL, retaining login and pending events.
Unhook,
/// Revoke the saved refresh-token family and remove the local session.
Logout,
/// Show local session metadata without exposing tokens.
Whoami,
/// Create a named provider webhook. Signing is enabled by default.
Create {
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
time_zone: Option<String>,
#[arg(
long,
help = "Signature policy JSON or @file; see hook docs signatures"
)]
signature: Option<String>,
#[arg(
long,
help = "BYOS secret file; '-' reads stdin. Preserves spaces; removes one trailing line ending"
)]
secret_file: Option<String>,
#[arg(long, help = "Accept unsigned requests for this hook")]
unsigned: bool,
},
/// Set or replace a BYOS secret without changing the hook URL or other settings.
SetSecret {
id: Uuid,
#[arg(
long,
help = "Secret file; '-' reads stdin. Preserves spaces; removes one trailing line ending"
)]
secret_file: String,
#[arg(long, value_parser = ["utf8", "ascii", "hex", "base64", "base64url", "raw"])]
secret_encoding: Option<String>,
},
/// List provider URLs, lifecycle state and latest receipt timestamps.
List {
#[arg(long)]
include_deleted: bool,
},
/// Read one hook's metadata and signing policy.
Show { id: Uuid },
/// Update metadata/signature policy with a JSON merge patch.
Update {
id: Uuid,
#[arg(
long,
help = "JSON or @file: name, description, time_zone, enabled, signature"
)]
patch: String,
},
/// Soft-delete a hook; recover it with restore within 45 days.
Delete { id: Uuid },
/// Recover a deleted hook within its 45-day retention window.
Restore { id: Uuid },
/// Resume ingress for one or more hooks atomically.
Enable {
#[arg(required = true)]
ids: Vec<Uuid>,
},
/// Pause ingress for one or more hooks while preserving their URLs and history.
Disable {
#[arg(required = true)]
ids: Vec<Uuid>,
},
/// Rotate a hook URL or signing secret.
Rotate {
#[command(subcommand)]
kind: Rotate,
},
/// Read verified request history; omit --hook for account-wide history.
Events(History),
/// Read withheld request history, retained for 14 days.
Blocked(History),
/// Pull, acknowledge or inspect durable delivery positions.
Deliveries {
#[command(subcommand)]
action: Deliveries,
},
/// Register this Silicon's IAM notifications as a Hook connection.
ConnectIam,
/// Read a live WebSocket stream; answer heartbeats automatically.
Listen {
#[arg(long, help = "Acknowledge each event after it is written to stdout")]
ack: bool,
},
/// Create, configure, reset and recover isolated testing environments.
Env {
#[command(subcommand)]
action: Environment,
},
/// Read or change local profile settings.
Config {
#[command(subcommand)]
action: Config,
},
/// Manage the persistent local relay and identity-specific subscriptions.
Daemon {
#[command(subcommand)]
action: Daemon,
},
/// Inspect backend readiness and API compatibility.
System {
#[command(subcommand)]
action: System,
},
/// Discover every command and its complete usage, without signing in.
Commands,
/// Read bundled guides: overview, api, client, cli, iam, signatures, testing, testing-api, testing-client, testing-cli, relay, contracts, configuration, telemetry, deployment.
Docs {
#[arg(default_value = "overview")]
topic: String,
},
}
#[derive(Debug, Args)]
#[command(
args_conflicts_with_subcommands = true,
subcommand_negates_reqs = true,
after_help = "Next: hook webhook <webhook-url> to receive events; hook login status --json to verify your identity."
)]
pub struct Login {
#[command(subcommand)]
pub action: Option<LoginAction>,
#[arg(
value_name = "SLT",
conflicts_with_all = ["slt", "slt_file"],
help = "IAM short-lived token (use --slt-file to avoid shell history)"
)]
pub token: Option<String>,
#[arg(
long,
conflicts_with_all = ["slt_file", "token"],
help = "Short-lived IAM token; --slt-file avoids shell history"
)]
pub slt: Option<String>,
#[arg(
long,
required_unless_present_any = ["slt", "token"],
help = "File containing only the short-lived token; '-' reads stdin"
)]
pub slt_file: Option<String>,
#[arg(
long,
help = "Local recipient URL; retained locally and never sent to the backend"
)]
pub webhook_url: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum LoginAction {
/// Verify the selected identity online with IAM; does not expose tokens.
Status,
}
#[derive(Debug, Subcommand)]
pub enum Rotate {
Endpoint { id: Uuid },
Secret { id: Uuid },
}
#[derive(Debug, Args)]
pub struct History {
#[arg(long)]
pub hook: Option<Uuid>,
#[arg(long, default_value_t = 100, help = "Number of requests, 1–10000")]
pub limit: u32,
#[arg(long, help = "Opaque next_cursor returned by a prior page")]
pub cursor: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum Deliveries {
/// Pull pending events without acknowledging them.
List {
#[arg(long, default_value_t = 100)]
limit: u32,
#[arg(long)]
after: Option<i64>,
},
/// Confirm all deliveries through this contiguous sequence.
Ack { through: i64 },
/// Read this identity's stored acknowledgment cursor.
Cursor,
}
#[derive(Debug, Subcommand)]
pub enum Environment {
/// Select a sandbox with its IAM app_secret. Then run `hook login <test-slt-or-id>`.
Use {
#[arg(long, help = "File containing the IAM app_secret; '-' reads stdin")]
app_secret_file: String,
},
/// Leave testing mode and return to the separately saved production session.
Exit,
/// Create an empty Hook sandbox linked to an existing IAM test environment.
Create {
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
iam_key_file: String,
#[arg(
long,
help = "Optional JSON file with test app_id, app_secret, webhook_secret, webhook_secret_version"
)]
iam_config: Option<String>,
},
/// Store a root key for an existing environment, without changing backend data.
Attach {
id: Uuid,
#[arg(long)]
key_file: String,
},
/// List environments owned by the production organization.
List {
#[arg(long,default_value="active",value_parser=["active","deleted","all"])]
status: String,
#[arg(long, default_value_t = 100, help = "Page size, 1–1000")]
limit: u32,
#[arg(long, help = "Last environment UUID from the previous descending page")]
after: Option<Uuid>,
},
Show {
id: Uuid,
},
/// Retrieve a key as creator/org administrator; JSON output includes the secret.
Key {
id: Uuid,
},
/// Invalidate an environment root key and store the replacement locally.
RotateKey {
id: Uuid,
},
/// Soft-delete the environment; recoverable for 30 days.
Delete {
id: Uuid,
},
Restore {
id: Uuid,
},
/// Read the selected test environment. Requires --test.
Current,
/// Erase Hook data while retaining the environment and IAM binding. Requires --test.
Clean,
/// Install test-only IAM application credentials from a JSON file. Requires --test.
ConfigureIam {
#[arg(long)]
file: String,
},
}
#[derive(Debug, Subcommand)]
pub enum Config {
/// Show configuration and identity metadata; credentials are redacted.
Show,
/// List locally configured profile names.
Profiles,
/// Set the base home directory; Hook stores state below .silicon-hook.
Home { location: String },
/// Set url, org, silicon or telemetry for the selected profile.
Set {
#[arg(value_parser=["url","org","silicon","telemetry"])]
key: String,
value: String,
},
}
#[derive(Debug, Subcommand)]
pub enum System {
Version,
Health,
}
#[derive(Debug, Subcommand)]
pub enum Daemon {
/// Start the background relay, if it is not already running.
Start,
/// Run in the foreground; useful under a service manager.
Run,
/// Show daemon health and number of configured identities.
Status,
/// Stop the relay; backend events remain pending for replay.
Stop,
/// Replace the selected identity's subscribed Silicons. No IDs unsubscribes all.
Subscribe { silicons: Vec<String> },
/// Print the selected identity's local API token. Treat the output as a secret.
Token,
/// Send a LocalRequest JSON file through this identity and echo the exact receipt.
Request {
#[arg(long)]
file: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn login_supports_token_only_and_status_without_tokens() {
let cli = Cli::try_parse_from(["hook", "login", "opaque-slt"]).unwrap();
assert!(matches!(
cli.command,
Command::Login(Login {
token: Some(_),
webhook_url: None,
..
})
));
let cli = Cli::try_parse_from(["hook", "login", "status", "--json"]).unwrap();
assert!(cli.json);
assert!(matches!(
cli.command,
Command::Login(Login {
action: Some(LoginAction::Status),
..
})
));
assert!(Cli::try_parse_from(["hook", "login"]).is_err());
assert!(Cli::try_parse_from(["hook", "login", "status", "--slt", "secret"]).is_err());
}
#[test]
fn delivery_and_discovery_support_test_context() {
for command in [
vec!["iam", "--json"],
vec!["webhook", "http://127.0.0.1/events"],
vec!["unhook"],
] {
let mut args = vec![
"hook",
"--profile",
"reviewer",
"--test",
"00000000-0000-4000-8000-000000000001",
];
args.extend(command);
let cli = Cli::try_parse_from(args).unwrap();
assert!(cli.test.is_some());
assert_eq!(cli.profile, "reviewer");
}
}
#[test]
fn byos_commands_support_files_stdin_and_test_context() {
let id = "00000000-0000-4000-8000-000000000001";
for command in [
vec!["create", "Stripe", "--secret-file", "-"],
vec![
"set-secret",
id,
"--secret-file",
"provider.txt",
"--secret-encoding",
"hex",
],
] {
let mut args = vec!["hook", "--test", id];
args.extend(command);
assert!(Cli::try_parse_from(args).is_ok());
}
assert!(Cli::try_parse_from(["hook", "set-secret", id]).is_err());
assert!(
Cli::try_parse_from([
"hook",
"set-secret",
id,
"--secret-file",
"-",
"--secret-encoding",
"invalid"
])
.is_err()
);
}
}