@kashdao/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/CONTRIBUTING.md +159 -0
  3. package/LICENSE +21 -0
  4. package/README.md +868 -0
  5. package/SECURITY.md +110 -0
  6. package/dist/account-AVFLEM5D.js +90 -0
  7. package/dist/account-AVFLEM5D.js.map +1 -0
  8. package/dist/auth-GUJCVKTD.js +239 -0
  9. package/dist/auth-GUJCVKTD.js.map +1 -0
  10. package/dist/chunk-BN2CUM42.js +9 -0
  11. package/dist/chunk-BN2CUM42.js.map +1 -0
  12. package/dist/chunk-BRK7KJ4O.js +154 -0
  13. package/dist/chunk-BRK7KJ4O.js.map +1 -0
  14. package/dist/chunk-KMBMQIZ7.js +393 -0
  15. package/dist/chunk-KMBMQIZ7.js.map +1 -0
  16. package/dist/chunk-LBIRQHX5.js +133 -0
  17. package/dist/chunk-LBIRQHX5.js.map +1 -0
  18. package/dist/chunk-MIXOZU2S.js +366 -0
  19. package/dist/chunk-MIXOZU2S.js.map +1 -0
  20. package/dist/chunk-QJMF73M5.js +129 -0
  21. package/dist/chunk-QJMF73M5.js.map +1 -0
  22. package/dist/chunk-UZNSYATZ.js +680 -0
  23. package/dist/chunk-UZNSYATZ.js.map +1 -0
  24. package/dist/chunk-VIADBYFY.js +94 -0
  25. package/dist/chunk-VIADBYFY.js.map +1 -0
  26. package/dist/chunk-YHCG2SUC.js +159 -0
  27. package/dist/chunk-YHCG2SUC.js.map +1 -0
  28. package/dist/chunk-YJX3JJ4M.js +174 -0
  29. package/dist/chunk-YJX3JJ4M.js.map +1 -0
  30. package/dist/client-IOM55ZCS.js +14 -0
  31. package/dist/client-IOM55ZCS.js.map +1 -0
  32. package/dist/completion-ZZGX5BQC.js +100 -0
  33. package/dist/completion-ZZGX5BQC.js.map +1 -0
  34. package/dist/config-XJL5TSYK.js +690 -0
  35. package/dist/config-XJL5TSYK.js.map +1 -0
  36. package/dist/config-store-3ZSYGXMQ.js +44 -0
  37. package/dist/config-store-3ZSYGXMQ.js.map +1 -0
  38. package/dist/docs-MDKKSJDC.js +102 -0
  39. package/dist/docs-MDKKSJDC.js.map +1 -0
  40. package/dist/eoa-IQ72EIHR.js +551 -0
  41. package/dist/eoa-IQ72EIHR.js.map +1 -0
  42. package/dist/errors-WMZIEGQI.js +18 -0
  43. package/dist/errors-WMZIEGQI.js.map +1 -0
  44. package/dist/explain-H4EH3KH3.js +165 -0
  45. package/dist/explain-H4EH3KH3.js.map +1 -0
  46. package/dist/global-options-XOLJUPTT.js +18 -0
  47. package/dist/global-options-XOLJUPTT.js.map +1 -0
  48. package/dist/health-VZEIII74.js +98 -0
  49. package/dist/health-VZEIII74.js.map +1 -0
  50. package/dist/help-footer-GTANVDNP.js +43 -0
  51. package/dist/help-footer-GTANVDNP.js.map +1 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +251 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/intro-PQQTYWR7.js +41 -0
  56. package/dist/intro-PQQTYWR7.js.map +1 -0
  57. package/dist/markets-C37JZDPE.js +295 -0
  58. package/dist/markets-C37JZDPE.js.map +1 -0
  59. package/dist/output-VCBZ3FM7.js +22 -0
  60. package/dist/output-VCBZ3FM7.js.map +1 -0
  61. package/dist/portfolio-2AEPIJIG.js +115 -0
  62. package/dist/portfolio-2AEPIJIG.js.map +1 -0
  63. package/dist/protocol-XEQXF2GW.js +1572 -0
  64. package/dist/protocol-XEQXF2GW.js.map +1 -0
  65. package/dist/quote-I4DVJ7ZB.js +144 -0
  66. package/dist/quote-I4DVJ7ZB.js.map +1 -0
  67. package/dist/schema-PIFQ65TS.js +410 -0
  68. package/dist/schema-PIFQ65TS.js.map +1 -0
  69. package/dist/setup-USZ6IODD.js +260 -0
  70. package/dist/setup-USZ6IODD.js.map +1 -0
  71. package/dist/stdin-YW2CEQXU.js +28 -0
  72. package/dist/stdin-YW2CEQXU.js.map +1 -0
  73. package/dist/trace-IZBYTUFO.js +103 -0
  74. package/dist/trace-IZBYTUFO.js.map +1 -0
  75. package/dist/trade-IIZSEXEI.js +586 -0
  76. package/dist/trade-IIZSEXEI.js.map +1 -0
  77. package/dist/version-JDK3PEP5.js +117 -0
  78. package/dist/version-JDK3PEP5.js.map +1 -0
  79. package/dist/version-check-TDCION37.js +138 -0
  80. package/dist/version-check-TDCION37.js.map +1 -0
  81. package/dist/webhooks-PTVRKICZ.js +680 -0
  82. package/dist/webhooks-PTVRKICZ.js.map +1 -0
  83. package/dist/with-retry-4FIZG3A7.js +223 -0
  84. package/dist/with-retry-4FIZG3A7.js.map +1 -0
  85. package/package.json +99 -0
@@ -0,0 +1,680 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatDate,
4
+ shortId
5
+ } from "./chunk-BRK7KJ4O.js";
6
+ import {
7
+ buildClient
8
+ } from "./chunk-QJMF73M5.js";
9
+ import "./chunk-KMBMQIZ7.js";
10
+ import "./chunk-YHCG2SUC.js";
11
+ import "./chunk-BN2CUM42.js";
12
+ import {
13
+ parsePositiveInt,
14
+ readGlobals
15
+ } from "./chunk-LBIRQHX5.js";
16
+ import {
17
+ log,
18
+ print,
19
+ printJson,
20
+ style,
21
+ writeNdjson
22
+ } from "./chunk-VIADBYFY.js";
23
+ import "./chunk-MIXOZU2S.js";
24
+ import {
25
+ CliError,
26
+ CliValidationError,
27
+ toCliError
28
+ } from "./chunk-UZNSYATZ.js";
29
+
30
+ // src/commands/webhooks/index.ts
31
+ import { Command as Command6 } from "commander";
32
+
33
+ // src/commands/webhooks/list.ts
34
+ import { Command } from "commander";
35
+ var STATUS_VALUES = ["none", "pending", "delivered", "retrying", "failed"];
36
+ var listWebhookEventsCommand = new Command("list").description("List recent webhook delivery events for the calling key.").option("-l, --limit <n>", "page size (1-100)", "20").option("-c, --cursor <cursor>", "opaque cursor from a previous response").option("-a, --all", "walk every page (subject to --limit per page)", false).option(
37
+ "--ndjson",
38
+ "stream results as newline-delimited JSON (one record per line); implies --all"
39
+ ).option(
40
+ "--status <status...>",
41
+ `filter by derived status (one of: ${STATUS_VALUES.join(", ")}); repeat or comma-separate`
42
+ ).addHelpText(
43
+ "after",
44
+ `
45
+ Examples:
46
+ $ kash webhooks list
47
+ $ kash webhooks list --status failed --status retrying
48
+ $ kash webhooks list --status failed,retrying -l 100
49
+ $ kash webhooks list --all --json | jq '.[] | select(.status == "failed")'
50
+ $ kash webhooks list --ndjson | while read -r line; do echo "$line" | jq -r .id; done
51
+ `
52
+ ).action(async (opts, cmd) => {
53
+ const globals = readGlobals(cmd);
54
+ const limit = opts.limit === void 0 ? 20 : parsePositiveInt(opts.limit, "limit");
55
+ if (limit < 1 || limit > 100) {
56
+ throw new CliValidationError("--limit must be between 1 and 100.", void 0, "limit");
57
+ }
58
+ const status = normaliseStatus(opts.status);
59
+ try {
60
+ const { client } = await buildClient({ requireAuth: true, globals });
61
+ const page = await client.webhooks.list({
62
+ limit,
63
+ ...opts.cursor === void 0 ? {} : { cursor: opts.cursor },
64
+ ...status === void 0 ? {} : { status }
65
+ });
66
+ if (opts.ndjson === true) {
67
+ for await (const event of page) {
68
+ writeNdjson(event);
69
+ }
70
+ return;
71
+ }
72
+ if (opts.all === true) {
73
+ const collected = [];
74
+ for await (const event of page) collected.push(event);
75
+ if (globals.json) {
76
+ printJson({
77
+ data: collected,
78
+ pagination: { hasMore: false, cursor: null },
79
+ count: collected.length
80
+ });
81
+ return;
82
+ }
83
+ renderRows(collected);
84
+ return;
85
+ }
86
+ if (globals.json) {
87
+ printJson({
88
+ data: page.data,
89
+ pagination: page.pagination,
90
+ count: page.data.length
91
+ });
92
+ return;
93
+ }
94
+ renderRows(page.data);
95
+ if (page.pagination.hasMore && page.pagination.cursor) {
96
+ log.detail("Next page", `--cursor ${page.pagination.cursor}`);
97
+ }
98
+ } catch (cause) {
99
+ throw toCliError(cause);
100
+ }
101
+ });
102
+ function renderRows(rows) {
103
+ if (rows.length === 0) {
104
+ print(style.dim(" (no webhook events)"));
105
+ return;
106
+ }
107
+ print("");
108
+ for (const row of rows) {
109
+ const statusBadge = renderStatus(row.status);
110
+ const tradeCol = row.tradeRequestId ? row.tradeRequestId.slice(0, 8) : style.dim("\u2014");
111
+ print(
112
+ ` ${statusBadge} ${style.dim(row.id.slice(0, 8))} ${row.eventType.padEnd(28)} trade=${tradeCol} attempts=${String(row.delivery.attempts).padStart(2)} ${style.dim(row.emittedAt)}`
113
+ );
114
+ }
115
+ print("");
116
+ }
117
+ function normaliseStatus(raw) {
118
+ if (!raw || raw.length === 0) return void 0;
119
+ const seen = /* @__PURE__ */ new Set();
120
+ for (const entry of raw) {
121
+ for (const piece of entry.split(",")) {
122
+ const trimmed = piece.trim();
123
+ if (trimmed.length === 0) continue;
124
+ if (!STATUS_VALUES.includes(trimmed)) {
125
+ throw new CliValidationError(
126
+ `Unknown --status "${trimmed}".`,
127
+ `Allowed: ${STATUS_VALUES.join(", ")}.`,
128
+ "status"
129
+ );
130
+ }
131
+ seen.add(trimmed);
132
+ }
133
+ }
134
+ return seen.size === 0 ? void 0 : [...seen];
135
+ }
136
+ function renderStatus(status) {
137
+ switch (status) {
138
+ case "delivered":
139
+ return style.success("\u2713 delivered");
140
+ case "failed":
141
+ return style.error("\u2717 failed ");
142
+ case "retrying":
143
+ return style.warn("\u27F3 retrying ");
144
+ case "pending":
145
+ return style.dim("\u2026 pending ");
146
+ case "none":
147
+ return style.dim("\xB7 none ");
148
+ default: {
149
+ const _exhaustive = status;
150
+ return _exhaustive;
151
+ }
152
+ }
153
+ }
154
+
155
+ // src/commands/webhooks/redeliver.ts
156
+ import { Command as Command2 } from "commander";
157
+ var EVENT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
158
+ var redeliverCommand = new Command2("redeliver").description("Re-queue a webhook event for delivery.").argument("<eventId>", "webhook event UUID").addHelpText(
159
+ "after",
160
+ `
161
+ Examples:
162
+ $ kash webhooks redeliver 99999999-9999-9999-9999-999999999999
163
+ $ kash webhooks redeliver "$(kash webhooks list --json --quiet | jq -r '.data[0].id')"
164
+ `
165
+ ).action(async (eventId, _opts, cmd) => {
166
+ const globals = readGlobals(cmd);
167
+ if (!EVENT_ID_REGEX.test(eventId)) {
168
+ throw new CliValidationError(
169
+ `<eventId> must be a UUID.`,
170
+ `Got "${eventId}". Look up the id in \`kash webhooks list --json --quiet | jq -r '.data[].id'\`.`,
171
+ "eventId"
172
+ );
173
+ }
174
+ let event;
175
+ try {
176
+ const { client } = await buildClient({ requireAuth: true, globals });
177
+ event = await client.webhooks.redeliver(eventId);
178
+ } catch (cause) {
179
+ throw toCliError(cause);
180
+ }
181
+ if (globals.json) {
182
+ printJson(event);
183
+ return;
184
+ }
185
+ log.success(`Re-delivery queued for ${event.id}.`);
186
+ print(` ${style.dim("Event id ")} ${event.id}`);
187
+ print(` ${style.dim("Event type ")} ${event.eventType}`);
188
+ print(` ${style.dim("Emitted at ")} ${formatDate(event.emittedAt)}`);
189
+ print(` ${style.dim("Attempts ")} ${String(event.deliveryAttempts)}`);
190
+ if (event.lastDeliveredAt) {
191
+ print(` ${style.dim("Last delivered ")} ${formatDate(event.lastDeliveredAt)}`);
192
+ }
193
+ log.detail("Short id", shortId(event.id));
194
+ });
195
+
196
+ // src/commands/webhooks/replay.ts
197
+ import { createHmac } from "crypto";
198
+ import { readFile } from "fs/promises";
199
+ import { Command as Command3 } from "commander";
200
+ var SIGNATURE_HEADER_DEFAULT = "X-Kash-Signature";
201
+ var replayWebhookCommand = new Command3("replay").description("Re-sign a captured webhook payload and POST it to a target URL.").argument("[body]", 'path to a JSON body file (omit or pass "-" for stdin)').requiredOption("-t, --target <url>", "destination URL (e.g. ngrok tunnel or localhost endpoint)").option("-s, --secret <secret>", "webhook signing secret (overrides KASH_WEBHOOK_SECRET)").option(
202
+ "--secret-file <path>",
203
+ "read the signing secret from a file (preferred \u2014 keeps the value out of argv and env)"
204
+ ).option(
205
+ "--secret-env <name>",
206
+ "read the signing secret from this environment variable (default: KASH_WEBHOOK_SECRET)"
207
+ ).option(
208
+ "--timestamp-ms <ms>",
209
+ "unix-ms timestamp for the signature header (default: current time)"
210
+ ).option(
211
+ "--signature-header <name>",
212
+ `override the signature header name (default: ${SIGNATURE_HEADER_DEFAULT})`
213
+ ).option("--timeout-ms <ms>", "fetch timeout (default 10000)").option(
214
+ "--dry-run",
215
+ "compute the signature header and inspect the would-be POST without sending it"
216
+ ).option(
217
+ "--refuse-private-addresses",
218
+ "hard-fail (instead of warning) when --target is a loopback / private / link-local address \u2014 recommended for CI"
219
+ ).addHelpText(
220
+ "after",
221
+ `
222
+ Examples:
223
+ # Auto-reads KASH_WEBHOOK_SECRET (matches \`kash webhooks verify\`).
224
+ $ KASH_WEBHOOK_SECRET=whsec_\u2026 kash webhooks replay payload.json -t http://localhost:3000/webhook
225
+
226
+ # Explicit secret file (preferred \u2014 keeps the value out of argv/env).
227
+ $ kash webhooks replay payload.json -t http://localhost:3000/webhook --secret-file ~/.kash/webhook.secret
228
+
229
+ # Pipe captured payload from stdin.
230
+ $ cat payload.json | kash webhooks replay - -t \u2026 -s whsec_\u2026
231
+
232
+ # CI guardrails: refuse private targets + inspect the signed bytes
233
+ # before sending.
234
+ $ kash webhooks replay payload.json -t https://api.example.com --secret-file s.txt --refuse-private-addresses
235
+ $ kash webhooks replay payload.json -t https://api.example.com --secret-file s.txt --dry-run --json
236
+
237
+ Notes:
238
+ - The signature format is \`t=<unix-ms>,v1=<hex-hmac-sha256>\` \u2014
239
+ Stripe-compatible. The receiver verifies via \`kash webhooks verify\`
240
+ or the SDK's \`webhooks.verifySignature\`.
241
+ - Defaults the timestamp to \`Date.now()\`. Pass \`--timestamp-ms\`
242
+ with an old timestamp to test the receiver's replay-window
243
+ rejection (the SDK's default is 5 min).
244
+ - Secret resolution order: --secret > --secret-file > --secret-env
245
+ > KASH_WEBHOOK_SECRET. --secret-file is preferred \u2014 it keeps the
246
+ value out of argv (where \`ps\` could see it) and out of env (where
247
+ a child process would inherit it).
248
+ - --dry-run is safe to run anywhere \u2014 it never opens a network
249
+ socket. Use it to inspect the signature header before pointing
250
+ a fresh receiver at production traffic.
251
+ `
252
+ ).action(async (body, options, cmd) => {
253
+ const globals = readGlobals(cmd);
254
+ try {
255
+ const secret = await resolveSecret(options);
256
+ const targetUrl = validateTarget(options.target, {
257
+ refusePrivate: options.refusePrivateAddresses === true
258
+ });
259
+ const rawBody = await readBody(body);
260
+ const timestampMs = options.timestampMs ? parsePositiveInt(options.timestampMs, "timestamp-ms") : Date.now();
261
+ const timeoutMs = options.timeoutMs ? parsePositiveInt(options.timeoutMs, "timeout-ms") : 1e4;
262
+ const headerName = options.signatureHeader ?? SIGNATURE_HEADER_DEFAULT;
263
+ const headerValue = computeSignatureHeader(rawBody, secret, timestampMs);
264
+ if (options.dryRun === true) {
265
+ if (globals.json) {
266
+ printJson({
267
+ targetUrl,
268
+ headerName,
269
+ headerValue,
270
+ timestampMs,
271
+ bodyBytes: Buffer.byteLength(rawBody, "utf8"),
272
+ dryRun: true
273
+ });
274
+ return;
275
+ }
276
+ print("");
277
+ log.success("Dry run \u2014 signature computed, no request sent.");
278
+ print(` ${style.dim("Target ")} ${targetUrl}`);
279
+ print(` ${style.dim("Header ")} ${headerName}: ${headerValue}`);
280
+ print(` ${style.dim("Body ")} ${String(Buffer.byteLength(rawBody, "utf8"))} bytes`);
281
+ log.info("Re-run without --dry-run to send.");
282
+ return;
283
+ }
284
+ const result = await deliver({
285
+ targetUrl,
286
+ body: rawBody,
287
+ headerName,
288
+ headerValue,
289
+ timeoutMs
290
+ });
291
+ if (globals.json) {
292
+ printJson({
293
+ targetUrl,
294
+ headerName,
295
+ headerValue,
296
+ timestampMs,
297
+ status: result.status,
298
+ ok: result.ok,
299
+ durationMs: result.durationMs,
300
+ responseBody: result.body
301
+ });
302
+ return;
303
+ }
304
+ print("");
305
+ if (result.ok) {
306
+ log.success(`Delivered in ${String(result.durationMs)}ms \u2014 HTTP ${String(result.status)}.`);
307
+ } else {
308
+ log.error(
309
+ `Receiver returned HTTP ${String(result.status)} (after ${String(result.durationMs)}ms).`
310
+ );
311
+ }
312
+ print(` ${style.dim("Target ")} ${targetUrl}`);
313
+ print(` ${style.dim("Header ")} ${headerName}: ${headerValue}`);
314
+ if (result.body.length > 0) {
315
+ print("");
316
+ print(style.dim("--- Response body ---"));
317
+ print(result.body);
318
+ }
319
+ } catch (cause) {
320
+ throw toCliError(cause);
321
+ }
322
+ });
323
+ async function resolveSecret(options) {
324
+ if (options.secret !== void 0) return options.secret;
325
+ if (options.secretFile !== void 0) {
326
+ try {
327
+ const raw = await readFile(options.secretFile, "utf8");
328
+ return raw.trim();
329
+ } catch {
330
+ throw new CliValidationError(
331
+ `--secret-file: failed to read ${options.secretFile}.`,
332
+ "Verify the file exists and is readable.",
333
+ "secret-file"
334
+ );
335
+ }
336
+ }
337
+ if (options.secretEnv !== void 0) {
338
+ const value = process.env[options.secretEnv];
339
+ if (!value || value.length === 0) {
340
+ throw new CliError(`--secret-env ${options.secretEnv} is not set or empty.`, {
341
+ code: "INVALID_INPUT",
342
+ recoverable: true,
343
+ suggestion: `Set ${options.secretEnv}=<webhook-secret> before re-running.`
344
+ });
345
+ }
346
+ return value;
347
+ }
348
+ const fromEnv = process.env["KASH_WEBHOOK_SECRET"];
349
+ if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
350
+ throw new CliValidationError(
351
+ "A signing secret is required.",
352
+ "Pass --secret-file (preferred), --secret, --secret-env <NAME>, or set KASH_WEBHOOK_SECRET.",
353
+ "secret"
354
+ );
355
+ }
356
+ function validateTarget(raw, policy) {
357
+ let url;
358
+ try {
359
+ url = new URL(raw);
360
+ } catch {
361
+ throw new CliValidationError(
362
+ "--target must be a valid URL.",
363
+ `Got "${raw}". Include the scheme (http:// or https://).`,
364
+ "target"
365
+ );
366
+ }
367
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
368
+ throw new CliValidationError(
369
+ `--target must use http:// or https:// (got "${url.protocol}").`,
370
+ void 0,
371
+ "target"
372
+ );
373
+ }
374
+ if (policy.refusePrivate && isPrivateAddress(url)) {
375
+ throw new CliValidationError(
376
+ `--target points at a private address (${url.hostname}) and --refuse-private-addresses is set.`,
377
+ "Drop the flag to allow private targets, or change --target to a public URL.",
378
+ "target"
379
+ );
380
+ }
381
+ warnIfPrivateAddress(url);
382
+ return url.toString();
383
+ }
384
+ function warnIfPrivateAddress(url) {
385
+ const rawHost = url.hostname;
386
+ const host = rawHost.startsWith("[") && rawHost.endsWith("]") ? rawHost.slice(1, -1) : rawHost;
387
+ if (host === "::1") {
388
+ emitPrivateWarning(rawHost);
389
+ return;
390
+ }
391
+ if (host === "localhost" || host.endsWith(".localhost")) return;
392
+ if (/^fe[89ab][0-9a-f]{0,2}:/i.test(host)) {
393
+ emitLinkLocalWarning(rawHost);
394
+ return;
395
+ }
396
+ if (/^f[cd][0-9a-f]{0,2}:/i.test(host)) {
397
+ emitPrivateWarning(rawHost);
398
+ return;
399
+ }
400
+ const ipv4 = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/.exec(
401
+ host
402
+ );
403
+ if (!ipv4) return;
404
+ const [, a, b] = ipv4;
405
+ const oct1 = Number.parseInt(a, 10);
406
+ const oct2 = Number.parseInt(b, 10);
407
+ if (oct1 === 127) return;
408
+ if (oct1 === 10) {
409
+ emitPrivateWarning(host);
410
+ return;
411
+ }
412
+ if (oct1 === 172 && oct2 >= 16 && oct2 <= 31) {
413
+ emitPrivateWarning(host);
414
+ return;
415
+ }
416
+ if (oct1 === 192 && oct2 === 168) {
417
+ emitPrivateWarning(host);
418
+ return;
419
+ }
420
+ if (oct1 === 169 && oct2 === 254) {
421
+ emitLinkLocalWarning(host);
422
+ return;
423
+ }
424
+ }
425
+ function isPrivateAddress(url) {
426
+ const rawHost = url.hostname;
427
+ const host = rawHost.startsWith("[") && rawHost.endsWith("]") ? rawHost.slice(1, -1) : rawHost;
428
+ if (host === "::1") return true;
429
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
430
+ if (/^fe[89ab][0-9a-f]{0,2}:/i.test(host)) return true;
431
+ if (/^f[cd][0-9a-f]{0,2}:/i.test(host)) return true;
432
+ const ipv4 = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/.exec(
433
+ host
434
+ );
435
+ if (!ipv4) return false;
436
+ const oct1 = Number.parseInt(ipv4[1], 10);
437
+ const oct2 = Number.parseInt(ipv4[2], 10);
438
+ if (oct1 === 127) return true;
439
+ if (oct1 === 10) return true;
440
+ if (oct1 === 172 && oct2 >= 16 && oct2 <= 31) return true;
441
+ if (oct1 === 192 && oct2 === 168) return true;
442
+ if (oct1 === 169 && oct2 === 254) return true;
443
+ return false;
444
+ }
445
+ function emitLinkLocalWarning(host) {
446
+ process.stderr.write(
447
+ `\u26A0 --target points at a link-local address (${host}). This range hosts cloud metadata services. Verify the target is intentional.
448
+ `
449
+ );
450
+ }
451
+ function emitPrivateWarning(host) {
452
+ process.stderr.write(
453
+ `\u26A0 --target points at a private address (${host}). Verify this is your dev tunnel and not an internal service.
454
+ `
455
+ );
456
+ }
457
+ async function readBody(file) {
458
+ if (file !== void 0 && file !== "-") {
459
+ let raw;
460
+ try {
461
+ raw = await readFile(file, "utf8");
462
+ } catch (cause) {
463
+ throw new CliError(`Failed to read ${file}.`, {
464
+ code: "CONFIGURATION",
465
+ recoverable: true,
466
+ suggestion: "Verify the path exists and is readable.",
467
+ cause
468
+ });
469
+ }
470
+ const { stripBom: stripBom2 } = await import("./stdin-YW2CEQXU.js");
471
+ return stripBom2(raw);
472
+ }
473
+ const chunks = [];
474
+ for await (const chunk of process.stdin) {
475
+ if (typeof chunk === "string") chunks.push(Buffer.from(chunk));
476
+ else if (chunk instanceof Buffer) chunks.push(chunk);
477
+ else chunks.push(Buffer.from(chunk));
478
+ }
479
+ if (chunks.length === 0) {
480
+ throw new CliValidationError(
481
+ "No input received on stdin.",
482
+ "Pass a file path or pipe a webhook body into stdin."
483
+ );
484
+ }
485
+ const { stripBom } = await import("./stdin-YW2CEQXU.js");
486
+ return stripBom(Buffer.concat(chunks).toString("utf8"));
487
+ }
488
+ function computeSignatureHeader(body, secret, timestampMs) {
489
+ const payload = `${String(timestampMs)}.${body}`;
490
+ const hex = createHmac("sha256", secret).update(payload).digest("hex");
491
+ return `t=${String(timestampMs)},v1=${hex}`;
492
+ }
493
+ async function deliver(args) {
494
+ const start = Date.now();
495
+ const controller = new AbortController();
496
+ const timer = setTimeout(() => controller.abort(), args.timeoutMs);
497
+ try {
498
+ const response = await /* eslint-disable-line @kashdao/no-bare-fetch */
499
+ fetch(args.targetUrl, {
500
+ method: "POST",
501
+ headers: {
502
+ "Content-Type": "application/json",
503
+ [args.headerName]: args.headerValue
504
+ },
505
+ body: args.body,
506
+ signal: controller.signal
507
+ });
508
+ const responseBody = await response.text();
509
+ return {
510
+ status: response.status,
511
+ ok: response.ok,
512
+ body: responseBody,
513
+ durationMs: Date.now() - start
514
+ };
515
+ } catch (cause) {
516
+ const wasAborted = controller.signal.aborted;
517
+ const looksLikeAbort = cause instanceof Error && cause.name === "AbortError";
518
+ if (wasAborted || looksLikeAbort) {
519
+ throw new CliError(`Delivery timed out after ${String(args.timeoutMs)}ms.`, {
520
+ code: "TIMEOUT",
521
+ recoverable: true,
522
+ suggestion: "Raise --timeout-ms or check the target endpoint is responsive.",
523
+ cause
524
+ });
525
+ }
526
+ throw new CliError(
527
+ `Delivery failed: ${cause instanceof Error ? cause.message : String(cause)}`,
528
+ {
529
+ code: "NETWORK",
530
+ recoverable: true,
531
+ suggestion: "Verify the target URL is reachable from this machine.",
532
+ cause
533
+ }
534
+ );
535
+ } finally {
536
+ clearTimeout(timer);
537
+ }
538
+ }
539
+
540
+ // src/commands/webhooks/rotate-secret.ts
541
+ import readline from "readline/promises";
542
+ import { Command as Command4 } from "commander";
543
+ var rotateSecretCommand = new Command4("rotate-secret").description("Rotate the webhook signing secret for the current API key.").option("-y, --yes", "skip the interactive confirmation (required for --json --quiet)").addHelpText(
544
+ "after",
545
+ `
546
+ Examples:
547
+ $ kash webhooks rotate-secret # interactive confirm
548
+ $ kash webhooks rotate-secret --yes # skip confirm
549
+ $ kash webhooks rotate-secret --yes --json --quiet | jq -r '.secret' > new-secret.txt
550
+
551
+ Notes:
552
+ This invalidates your current webhook secret immediately. Receivers
553
+ that haven't been updated will start failing signature verification
554
+ on the next event. Have your secret-store update plan ready before
555
+ confirming.
556
+ `
557
+ ).action(async (options, cmd) => {
558
+ const globals = readGlobals(cmd);
559
+ const interactive = process.stdin.isTTY && !globals.quiet && !globals.json;
560
+ const confirmed = options.yes === true || !interactive ? true : await confirm();
561
+ if (!confirmed) {
562
+ log.info("Aborted.");
563
+ return;
564
+ }
565
+ let secret;
566
+ try {
567
+ const { client } = await buildClient({ requireAuth: true, globals });
568
+ secret = await client.webhooks.rotateSecret();
569
+ } catch (cause) {
570
+ throw toCliError(cause);
571
+ }
572
+ if (globals.json) {
573
+ printJson(secret);
574
+ return;
575
+ }
576
+ log.warn("Webhook secret rotated. The new secret is shown ONCE.");
577
+ print("");
578
+ print(` ${style.bold("Secret")}: ${secret.secret}`);
579
+ print(` ${style.dim("Rotated at ")}: ${formatDate(secret.rotatedAt)}`);
580
+ print(` ${style.dim("Previous valid until")}: ${formatDate(secret.previousRetainedUntil)}`);
581
+ print("");
582
+ log.info("Store this secret in your webhook receiver immediately.");
583
+ });
584
+ async function confirm() {
585
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
586
+ try {
587
+ const answer = await rl.question(
588
+ "This will invalidate your current webhook secret. Continue? [y/N] "
589
+ );
590
+ return answer.trim().toLowerCase().startsWith("y");
591
+ } finally {
592
+ rl.close();
593
+ }
594
+ }
595
+
596
+ // src/commands/webhooks/verify.ts
597
+ import { readFile as readFile2 } from "fs/promises";
598
+ import { Command as Command5 } from "commander";
599
+ var verifyWebhookCommand = new Command5("verify").description("Verify a captured X-Kash-Signature against the raw body and secret.").requiredOption("-s, --signature <header>", 'the X-Kash-Signature header value (e.g. "t=\u2026,v1=\u2026")').option("--body <string>", "raw request body as a string (use --body-file for binary-safe input)").option("--body-file <path>", "path to a file containing the raw request body").option("--secret <value>", "webhook signing secret (overrides KASH_WEBHOOK_SECRET)").option(
600
+ "--secret-file <path>",
601
+ "read secret from a file (preferred over --secret on shared shells)"
602
+ ).option(
603
+ "--tolerance-ms <n>",
604
+ "replay-window tolerance in milliseconds (default 300000 = 5 minutes)"
605
+ ).addHelpText(
606
+ "after",
607
+ `
608
+ Examples:
609
+ $ kash webhooks verify --signature "t=\u2026,v1=\u2026" --body-file event.json
610
+ $ kash webhooks verify -s "$SIG" --body "$BODY" --secret-file ~/.kash/webhook.secret --json
611
+ `
612
+ ).action(async (options, cmd) => {
613
+ const globals = readGlobals(cmd);
614
+ if (options.body !== void 0 && options.bodyFile !== void 0) {
615
+ throw new CliValidationError("Pass exactly one of --body or --body-file.", void 0, "body");
616
+ }
617
+ if (options.body === void 0 && options.bodyFile === void 0) {
618
+ throw new CliValidationError("--body or --body-file is required.", void 0, "body");
619
+ }
620
+ if (options.secret !== void 0 && options.secretFile !== void 0) {
621
+ throw new CliValidationError(
622
+ "Pass at most one of --secret or --secret-file.",
623
+ void 0,
624
+ "secret"
625
+ );
626
+ }
627
+ const body = options.body !== void 0 ? options.body : await readFileChecked(options.bodyFile, "body-file");
628
+ const secret = await resolveSecret2(options);
629
+ const toleranceMs = options.toleranceMs === void 0 ? void 0 : parsePositiveInt(options.toleranceMs, "tolerance-ms");
630
+ let result;
631
+ try {
632
+ const { client } = await buildClient({ globals });
633
+ result = await client.webhooks.verifySignature(body, options.signature, secret, {
634
+ ...toleranceMs === void 0 ? {} : { toleranceMs }
635
+ });
636
+ } catch (cause) {
637
+ throw toCliError(cause);
638
+ }
639
+ if (globals.json) {
640
+ printJson(result);
641
+ return;
642
+ }
643
+ if (result.valid) {
644
+ print(`${style.success("\u2713")} Signature is valid.`);
645
+ } else {
646
+ log.error(`Signature is INVALID: ${result.reason}`);
647
+ }
648
+ });
649
+ async function readFileChecked(path, flag) {
650
+ try {
651
+ return await readFile2(path, "utf8");
652
+ } catch (cause) {
653
+ throw new CliValidationError(
654
+ `Failed to read ${flag} ${path}: ${cause.message}`,
655
+ "Verify the path exists and is readable.",
656
+ flag
657
+ );
658
+ }
659
+ }
660
+ async function resolveSecret2(options) {
661
+ if (options.secret !== void 0) return options.secret;
662
+ if (options.secretFile !== void 0) {
663
+ const raw = await readFileChecked(options.secretFile, "secret-file");
664
+ return raw.trim();
665
+ }
666
+ const fromEnv = process.env["KASH_WEBHOOK_SECRET"];
667
+ if (fromEnv !== void 0 && fromEnv.length > 0) return fromEnv;
668
+ throw new CliValidationError(
669
+ "Webhook secret not provided.",
670
+ "Pass --secret-file (preferred), --secret, or set KASH_WEBHOOK_SECRET.",
671
+ "secret"
672
+ );
673
+ }
674
+
675
+ // src/commands/webhooks/index.ts
676
+ var webhooksCommand = new Command6("webhooks").description("Manage webhook delivery, signing secrets, and signature verification.").addCommand(listWebhookEventsCommand).addCommand(rotateSecretCommand).addCommand(redeliverCommand).addCommand(verifyWebhookCommand).addCommand(replayWebhookCommand);
677
+ export {
678
+ webhooksCommand
679
+ };
680
+ //# sourceMappingURL=webhooks-PTVRKICZ.js.map