@dharta/cli 0.6.77 → 0.6.78

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 (293) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +215 -11
  3. package/dist/agent.js +3 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/api-error.js +6 -6
  6. package/dist/api-error.js.map +1 -1
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +860 -149
  9. package/dist/cli.js.map +1 -1
  10. package/dist/command-tree.d.ts +3 -0
  11. package/dist/command-tree.js +10 -0
  12. package/dist/command-tree.js.map +1 -0
  13. package/dist/commands/agents.d.ts +54 -17
  14. package/dist/commands/agents.js +246 -136
  15. package/dist/commands/agents.js.map +1 -1
  16. package/dist/commands/analytics.js +6 -5
  17. package/dist/commands/analytics.js.map +1 -1
  18. package/dist/commands/api.js +71 -86
  19. package/dist/commands/api.js.map +1 -1
  20. package/dist/commands/appearance.js +5 -6
  21. package/dist/commands/appearance.js.map +1 -1
  22. package/dist/commands/artifact.js +7 -6
  23. package/dist/commands/artifact.js.map +1 -1
  24. package/dist/commands/audit-log.js +2 -1
  25. package/dist/commands/audit-log.js.map +1 -1
  26. package/dist/commands/auth.js +26 -6
  27. package/dist/commands/auth.js.map +1 -1
  28. package/dist/commands/caps.js +11 -15
  29. package/dist/commands/caps.js.map +1 -1
  30. package/dist/commands/deploy.js +25 -10
  31. package/dist/commands/deploy.js.map +1 -1
  32. package/dist/commands/dev.d.ts +10 -1
  33. package/dist/commands/dev.js +56 -25
  34. package/dist/commands/dev.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +2 -0
  36. package/dist/commands/doctor.js +60 -0
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/embed-keys.js +4 -13
  39. package/dist/commands/embed-keys.js.map +1 -1
  40. package/dist/commands/env.js +4 -7
  41. package/dist/commands/env.js.map +1 -1
  42. package/dist/commands/files.js +3 -4
  43. package/dist/commands/files.js.map +1 -1
  44. package/dist/commands/fleet.d.ts +129 -0
  45. package/dist/commands/fleet.js +408 -0
  46. package/dist/commands/fleet.js.map +1 -0
  47. package/dist/commands/genui.d.ts +2 -0
  48. package/dist/commands/genui.js +53 -0
  49. package/dist/commands/genui.js.map +1 -0
  50. package/dist/commands/git-credential.js +2 -3
  51. package/dist/commands/git-credential.js.map +1 -1
  52. package/dist/commands/kartas.d.ts +9 -0
  53. package/dist/commands/kartas.js +33 -11
  54. package/dist/commands/kartas.js.map +1 -1
  55. package/dist/commands/keys.d.ts +17 -18
  56. package/dist/commands/keys.js +108 -19
  57. package/dist/commands/keys.js.map +1 -1
  58. package/dist/commands/login.js +2 -1
  59. package/dist/commands/login.js.map +1 -1
  60. package/dist/commands/members.d.ts +10 -0
  61. package/dist/commands/members.js +20 -6
  62. package/dist/commands/members.js.map +1 -1
  63. package/dist/commands/model-connections.d.ts +44 -0
  64. package/dist/commands/model-connections.js +435 -0
  65. package/dist/commands/model-connections.js.map +1 -0
  66. package/dist/commands/open.d.ts +2 -2
  67. package/dist/commands/open.js +4 -3
  68. package/dist/commands/open.js.map +1 -1
  69. package/dist/commands/org.d.ts +9 -0
  70. package/dist/commands/org.js +42 -0
  71. package/dist/commands/org.js.map +1 -0
  72. package/dist/commands/organization.d.ts +16 -0
  73. package/dist/commands/organization.js +118 -0
  74. package/dist/commands/organization.js.map +1 -0
  75. package/dist/commands/parity.js +1 -3
  76. package/dist/commands/parity.js.map +1 -1
  77. package/dist/commands/repos.d.ts +4 -0
  78. package/dist/commands/repos.js +17 -0
  79. package/dist/commands/repos.js.map +1 -0
  80. package/dist/commands/rollback.js +2 -3
  81. package/dist/commands/rollback.js.map +1 -1
  82. package/dist/commands/schedules.js +26 -21
  83. package/dist/commands/schedules.js.map +1 -1
  84. package/dist/commands/scopes.js +8 -1
  85. package/dist/commands/scopes.js.map +1 -1
  86. package/dist/commands/sessions.js +9 -3
  87. package/dist/commands/sessions.js.map +1 -1
  88. package/dist/commands/setup.js +5 -4
  89. package/dist/commands/setup.js.map +1 -1
  90. package/dist/commands/update.d.ts +1 -0
  91. package/dist/commands/update.js +24 -11
  92. package/dist/commands/update.js.map +1 -1
  93. package/dist/commands/webhooks.d.ts +4 -1
  94. package/dist/commands/webhooks.js +21 -16
  95. package/dist/commands/webhooks.js.map +1 -1
  96. package/dist/completions.js +24 -13
  97. package/dist/completions.js.map +1 -1
  98. package/dist/config.d.ts +12 -0
  99. package/dist/config.js +121 -7
  100. package/dist/config.js.map +1 -1
  101. package/dist/context.d.ts +2 -0
  102. package/dist/context.js +23 -7
  103. package/dist/context.js.map +1 -1
  104. package/dist/docs/cli-reference.d.ts +2 -0
  105. package/dist/docs/cli-reference.js +191 -0
  106. package/dist/docs/cli-reference.js.map +1 -0
  107. package/dist/exit-codes.d.ts +25 -0
  108. package/dist/exit-codes.js +107 -0
  109. package/dist/exit-codes.js.map +1 -0
  110. package/dist/genui.d.ts +178 -0
  111. package/dist/genui.js +1130 -0
  112. package/dist/genui.js.map +1 -0
  113. package/dist/harnesses.js +2 -1
  114. package/dist/harnesses.js.map +1 -1
  115. package/dist/jq.d.ts +4 -0
  116. package/dist/jq.js +91 -0
  117. package/dist/jq.js.map +1 -0
  118. package/dist/login-offer.js +6 -2
  119. package/dist/login-offer.js.map +1 -1
  120. package/dist/manifest.d.ts +12 -0
  121. package/dist/manifest.js +126 -0
  122. package/dist/manifest.js.map +1 -1
  123. package/dist/model-auth.d.ts +29 -0
  124. package/dist/model-auth.js +117 -0
  125. package/dist/model-auth.js.map +1 -0
  126. package/dist/model-settings.d.ts +23 -0
  127. package/dist/model-settings.js +186 -0
  128. package/dist/model-settings.js.map +1 -0
  129. package/dist/money.js +2 -1
  130. package/dist/money.js.map +1 -1
  131. package/dist/output.d.ts +6 -0
  132. package/dist/output.js +54 -0
  133. package/dist/output.js.map +1 -1
  134. package/dist/runtime.d.ts +1 -1
  135. package/dist/runtime.js +1 -1
  136. package/dist/scaffold.js +2 -1
  137. package/dist/scaffold.js.map +1 -1
  138. package/dist/validators.js +17 -11
  139. package/dist/validators.js.map +1 -1
  140. package/dist/version-check.d.ts +1 -1
  141. package/dist/version-check.js +1 -1
  142. package/dist/version-check.js.map +1 -1
  143. package/node_modules/@dharta/contracts/LICENSE +21 -0
  144. package/node_modules/@dharta/contracts/README.md +45 -0
  145. package/node_modules/@dharta/contracts/artifact-runtime.json +750 -0
  146. package/node_modules/@dharta/contracts/dist/activity.d.ts +62 -0
  147. package/node_modules/@dharta/contracts/dist/activity.d.ts.map +1 -0
  148. package/node_modules/@dharta/contracts/dist/activity.js +178 -0
  149. package/node_modules/@dharta/contracts/dist/activity.js.map +1 -0
  150. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts +20 -0
  151. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts.map +1 -0
  152. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js +53 -0
  153. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js.map +1 -0
  154. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts +37 -0
  155. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts.map +1 -0
  156. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js +5107 -0
  157. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js.map +1 -0
  158. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts +154 -0
  159. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts.map +1 -0
  160. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts +1670 -0
  161. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts.map +1 -0
  162. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js +1903 -0
  163. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js.map +1 -0
  164. package/node_modules/@dharta/contracts/dist/artifact-runtime.js +377 -0
  165. package/node_modules/@dharta/contracts/dist/artifact-runtime.js.map +1 -0
  166. package/node_modules/@dharta/contracts/dist/index.d.ts +6 -0
  167. package/node_modules/@dharta/contracts/dist/index.d.ts.map +1 -0
  168. package/node_modules/@dharta/contracts/dist/index.js +6 -0
  169. package/node_modules/@dharta/contracts/dist/index.js.map +1 -0
  170. package/node_modules/@dharta/contracts/dist/model-auth.d.ts +31 -0
  171. package/node_modules/@dharta/contracts/dist/model-auth.d.ts.map +1 -0
  172. package/node_modules/@dharta/contracts/dist/model-auth.js +45 -0
  173. package/node_modules/@dharta/contracts/dist/model-auth.js.map +1 -0
  174. package/node_modules/@dharta/contracts/dist/tasks.d.ts +44 -0
  175. package/node_modules/@dharta/contracts/dist/tasks.d.ts.map +1 -0
  176. package/node_modules/@dharta/contracts/dist/tasks.js +127 -0
  177. package/node_modules/@dharta/contracts/dist/tasks.js.map +1 -0
  178. package/node_modules/@dharta/contracts/dist/wire-events.d.ts +71 -0
  179. package/node_modules/@dharta/contracts/dist/wire-events.d.ts.map +1 -0
  180. package/node_modules/@dharta/contracts/dist/wire-events.js +48 -0
  181. package/node_modules/@dharta/contracts/dist/wire-events.js.map +1 -0
  182. package/node_modules/@dharta/contracts/model-auth-capabilities-v1.json +168 -0
  183. package/node_modules/@dharta/contracts/model-auth-reasons-v1.json +38 -0
  184. package/node_modules/@dharta/contracts/package.json +44 -0
  185. package/node_modules/@dharta/contracts/src/activity.ts +215 -0
  186. package/node_modules/@dharta/contracts/src/artifact-runtime-formats.ts +54 -0
  187. package/node_modules/@dharta/contracts/src/artifact-runtime-validators.generated.ts +29 -0
  188. package/node_modules/@dharta/contracts/src/artifact-runtime.generated.ts +2093 -0
  189. package/node_modules/@dharta/contracts/src/artifact-runtime.ts +520 -0
  190. package/node_modules/@dharta/contracts/src/index.ts +5 -0
  191. package/node_modules/@dharta/contracts/src/model-auth.ts +82 -0
  192. package/node_modules/@dharta/contracts/src/tasks.ts +142 -0
  193. package/node_modules/@dharta/contracts/src/wire-events.ts +150 -0
  194. package/node_modules/@dharta/contracts/wire-events.json +29 -0
  195. package/node_modules/@dharta/sdk/LICENSE +1 -1
  196. package/node_modules/@dharta/sdk/README.md +22 -8
  197. package/node_modules/@dharta/sdk/dist/errors.d.ts +9 -7
  198. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -1
  199. package/node_modules/@dharta/sdk/dist/errors.js +36 -18
  200. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -1
  201. package/node_modules/@dharta/sdk/dist/fleet.d.ts +69 -0
  202. package/node_modules/@dharta/sdk/dist/fleet.d.ts.map +1 -0
  203. package/node_modules/@dharta/sdk/dist/fleet.js +240 -0
  204. package/node_modules/@dharta/sdk/dist/fleet.js.map +1 -0
  205. package/node_modules/@dharta/sdk/dist/http.d.ts +6 -2
  206. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -1
  207. package/node_modules/@dharta/sdk/dist/http.js +71 -12
  208. package/node_modules/@dharta/sdk/dist/http.js.map +1 -1
  209. package/node_modules/@dharta/sdk/dist/idempotency.d.ts +12 -0
  210. package/node_modules/@dharta/sdk/dist/idempotency.d.ts.map +1 -0
  211. package/node_modules/@dharta/sdk/dist/idempotency.js +72 -0
  212. package/node_modules/@dharta/sdk/dist/idempotency.js.map +1 -0
  213. package/node_modules/@dharta/sdk/dist/index.d.ts +20 -6
  214. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -1
  215. package/node_modules/@dharta/sdk/dist/index.js +27 -4
  216. package/node_modules/@dharta/sdk/dist/index.js.map +1 -1
  217. package/node_modules/@dharta/sdk/dist/resources.d.ts +108 -13
  218. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -1
  219. package/node_modules/@dharta/sdk/dist/resources.js +403 -57
  220. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -1
  221. package/node_modules/@dharta/sdk/dist/types.d.ts +436 -16
  222. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -1
  223. package/node_modules/@dharta/sdk/dist/types.js +31 -2
  224. package/node_modules/@dharta/sdk/dist/types.js.map +1 -1
  225. package/node_modules/@dharta/sdk/package.json +9 -3
  226. package/node_modules/@dharta/sdk/src/errors.ts +41 -18
  227. package/node_modules/@dharta/sdk/src/fleet.ts +339 -0
  228. package/node_modules/@dharta/sdk/src/http.ts +110 -17
  229. package/node_modules/@dharta/sdk/src/idempotency.ts +101 -0
  230. package/node_modules/@dharta/sdk/src/index.ts +50 -11
  231. package/node_modules/@dharta/sdk/src/resources.ts +549 -68
  232. package/node_modules/@dharta/sdk/src/types.ts +629 -20
  233. package/package.json +16 -6
  234. package/src/agent.ts +3 -2
  235. package/src/api-error.ts +6 -5
  236. package/src/cli.ts +978 -163
  237. package/src/command-tree.ts +13 -0
  238. package/src/commands/agents.ts +365 -167
  239. package/src/commands/analytics.ts +6 -5
  240. package/src/commands/api.ts +73 -92
  241. package/src/commands/appearance.ts +5 -6
  242. package/src/commands/artifact.ts +7 -6
  243. package/src/commands/audit-log.ts +2 -1
  244. package/src/commands/auth.ts +25 -5
  245. package/src/commands/caps.ts +11 -15
  246. package/src/commands/deploy.ts +18 -4
  247. package/src/commands/dev.ts +86 -25
  248. package/src/commands/doctor.ts +69 -0
  249. package/src/commands/embed-keys.ts +4 -14
  250. package/src/commands/env.ts +4 -7
  251. package/src/commands/files.ts +3 -4
  252. package/src/commands/fleet.ts +672 -0
  253. package/src/commands/genui.ts +59 -0
  254. package/src/commands/git-credential.ts +2 -3
  255. package/src/commands/kartas.ts +45 -11
  256. package/src/commands/keys.ts +152 -35
  257. package/src/commands/login.ts +2 -1
  258. package/src/commands/members.ts +31 -6
  259. package/src/commands/model-connections.ts +547 -0
  260. package/src/commands/open.ts +5 -4
  261. package/src/commands/org.ts +57 -0
  262. package/src/commands/organization.ts +153 -0
  263. package/src/commands/parity.ts +1 -3
  264. package/src/commands/repos.ts +23 -0
  265. package/src/commands/rollback.ts +2 -3
  266. package/src/commands/schedules.ts +33 -22
  267. package/src/commands/scopes.ts +8 -1
  268. package/src/commands/sessions.ts +9 -3
  269. package/src/commands/setup.ts +5 -4
  270. package/src/commands/update.ts +28 -12
  271. package/src/commands/webhooks.ts +30 -19
  272. package/src/completions.ts +25 -13
  273. package/src/config.ts +141 -9
  274. package/src/context.ts +34 -7
  275. package/src/docs/cli-reference.ts +206 -0
  276. package/src/exit-codes.ts +122 -0
  277. package/src/genui.ts +1398 -0
  278. package/src/harnesses.ts +2 -1
  279. package/src/jq.ts +109 -0
  280. package/src/login-offer.ts +6 -2
  281. package/src/manifest.ts +134 -0
  282. package/src/model-auth.ts +173 -0
  283. package/src/model-settings.ts +222 -0
  284. package/src/money.ts +5 -1
  285. package/src/output.ts +60 -0
  286. package/src/runtime.ts +1 -1
  287. package/src/scaffold.ts +2 -1
  288. package/src/validators.ts +18 -11
  289. package/src/version-check.ts +1 -1
  290. package/dist/commands/model-keys.d.ts +0 -22
  291. package/dist/commands/model-keys.js +0 -222
  292. package/dist/commands/model-keys.js.map +0 -1
  293. package/src/commands/model-keys.ts +0 -278
@@ -1,58 +1,81 @@
1
1
  import type { ErrorBody } from "./types.js";
2
2
 
3
- // Base class so consumers can `catch (e: KartaError)` without
3
+ // Base class so consumers can `catch (e: DhartaError)` without
4
4
  // switching on status codes manually for the common cases.
5
- export class KartaError extends Error {
5
+ export class DhartaError extends Error {
6
6
  readonly status: number;
7
7
  readonly body: ErrorBody | string | null;
8
+ // Seconds the server asked the caller to wait, from the `Retry-After`
9
+ // header, when it sent one. Undefined otherwise, including when the header
10
+ // is an HTTP-date rather than a delay: a caller that wants to sleep needs a
11
+ // number, and guessing one from a clock this client cannot trust is worse
12
+ // than saying nothing. Only 429 sends it today.
13
+ readonly retryAfterSeconds?: number;
8
14
 
9
- constructor(message: string, status: number, body: ErrorBody | string | null) {
15
+ constructor(
16
+ message: string,
17
+ status: number,
18
+ body: ErrorBody | string | null,
19
+ retryAfterSeconds?: number,
20
+ ) {
10
21
  super(message);
11
- this.name = "KartaError";
22
+ this.name = "DhartaError";
12
23
  this.status = status;
13
24
  this.body = body;
25
+ this.retryAfterSeconds = retryAfterSeconds;
14
26
  }
15
27
  }
16
28
 
17
- export class KartaAuthError extends KartaError {
29
+ export class DhartaAuthError extends DhartaError {
18
30
  constructor(body: ErrorBody | string | null) {
19
31
  super("Unauthenticated or invalid API key", 401, body);
20
- this.name = "KartaAuthError";
32
+ this.name = "DhartaAuthError";
21
33
  }
22
34
  }
23
35
 
24
- export class KartaForbiddenError extends KartaError {
36
+ export class DhartaForbiddenError extends DhartaError {
25
37
  constructor(body: ErrorBody | string | null) {
26
38
  super("Forbidden — caller lacks required scope", 403, body);
27
- this.name = "KartaForbiddenError";
39
+ this.name = "DhartaForbiddenError";
28
40
  }
29
41
  }
30
42
 
31
- export class KartaNotFoundError extends KartaError {
43
+ export class DhartaNotFoundError extends DhartaError {
32
44
  constructor(body: ErrorBody | string | null) {
33
45
  super("Resource not found", 404, body);
34
- this.name = "KartaNotFoundError";
46
+ this.name = "DhartaNotFoundError";
35
47
  }
36
48
  }
37
49
 
38
- export class KartaValidationError extends KartaError {
50
+ export class DhartaValidationError extends DhartaError {
39
51
  constructor(body: ErrorBody | string | null) {
40
52
  super("Validation failed", 422, body);
41
- this.name = "KartaValidationError";
53
+ this.name = "DhartaValidationError";
42
54
  }
43
55
  }
44
56
 
45
57
  export function errorFor(
46
58
  status: number,
47
59
  body: ErrorBody | string | null,
48
- ): KartaError {
49
- if (status === 401) return new KartaAuthError(body);
50
- if (status === 403) return new KartaForbiddenError(body);
51
- if (status === 404) return new KartaNotFoundError(body);
52
- if (status === 422) return new KartaValidationError(body);
60
+ retryAfterSeconds?: number,
61
+ ): DhartaError {
62
+ if (status === 401) return new DhartaAuthError(body);
63
+ if (status === 403) return new DhartaForbiddenError(body);
64
+ if (status === 404) return new DhartaNotFoundError(body);
65
+ if (status === 422) return new DhartaValidationError(body);
53
66
  const msg =
54
67
  body && typeof body === "object" && "message" in body && body.message
55
68
  ? body.message
56
69
  : `Dharta API error ${status}`;
57
- return new KartaError(String(msg), status, body);
70
+ return new DhartaError(String(msg), status, body, retryAfterSeconds);
71
+ }
72
+
73
+ // `Retry-After` as a whole number of seconds, or undefined when the header is
74
+ // absent, an HTTP-date, or otherwise not a non-negative integer count.
75
+ export function parseRetryAfterSeconds(header: string | null): number | undefined {
76
+ if (header === null) return undefined;
77
+ const trimmed = header.trim();
78
+ if (!/^\d+$/.test(trimmed)) return undefined;
79
+ const seconds = Number(trimmed);
80
+ return Number.isFinite(seconds) ? seconds : undefined;
58
81
  }
@@ -0,0 +1,339 @@
1
+ // The fleet mutations (RFC 0048 PR7). Reads stay on the catalog endpoint; this
2
+ // file covers the writes the fleet console makes.
3
+ //
4
+ // Two scopes: `fleet:write` for routine work, and `fleet:operations:write`,
5
+ // which expires within 90 days, for the eight actions that move live releases:
6
+ // every operation verb past the preview, and every rollout verb.
7
+
8
+ import type { HttpClient } from "./http.js";
9
+ import type { MutationOptions } from "./idempotency.js";
10
+ import type {
11
+ FleetAnomalyFinding,
12
+ FleetExportParams,
13
+ FleetOperation,
14
+ FleetOperationParams,
15
+ FleetRollout,
16
+ FleetSavedView,
17
+ FleetSavedViewParams,
18
+ FleetSnoozeDuration,
19
+ FleetAnalyticsSaveParams,
20
+ } from "./types.js";
21
+
22
+ interface OperationEnvelope {
23
+ operation: FleetOperation;
24
+ }
25
+
26
+ interface RolloutEnvelope {
27
+ rollout: FleetRollout;
28
+ }
29
+
30
+ interface FindingEnvelope {
31
+ finding: FleetAnomalyFinding;
32
+ }
33
+
34
+ interface SavedViewEnvelope {
35
+ saved_view: FleetSavedView;
36
+ }
37
+
38
+ export interface FleetRollbackResult {
39
+ rollout: FleetRollout;
40
+ operation: FleetOperation;
41
+ }
42
+
43
+ export class FleetOperationsResource {
44
+ constructor(private readonly http: HttpClient) {}
45
+
46
+ // Freezes the selection and returns a draft. Nothing is applied until
47
+ // `confirm`, so this is safe to call to find out what an action would touch.
48
+ async create(
49
+ params: FleetOperationParams,
50
+ options: MutationOptions = {},
51
+ ): Promise<FleetOperation> {
52
+ const res = await this.http.request<OperationEnvelope>({
53
+ method: "POST",
54
+ path: "/api/fleet/operations",
55
+ idempotencyKey: options.idempotencyKey,
56
+ body: { operation: params },
57
+ });
58
+ return res.operation;
59
+ }
60
+
61
+ // `phrase` is the `confirmation_phrase` the draft returned. It is derived
62
+ // from the frozen manifest, which does not change after the preview, so the
63
+ // phrase proves the caller saw that preview. It says nothing about whether
64
+ // the fleet has moved since.
65
+ async confirm(
66
+ id: string,
67
+ phrase: string,
68
+ options: MutationOptions = {},
69
+ ): Promise<FleetOperation> {
70
+ return this.post(id, "confirm", { confirmation_phrase: phrase }, options);
71
+ }
72
+
73
+ async cancel(id: string, options: MutationOptions = {}): Promise<FleetOperation> {
74
+ return this.post(id, "cancel", undefined, options);
75
+ }
76
+
77
+ async retry(id: string, options: MutationOptions = {}): Promise<FleetOperation> {
78
+ return this.post(id, "retry", undefined, options);
79
+ }
80
+
81
+ private async post(
82
+ id: string,
83
+ action: string,
84
+ body?: unknown,
85
+ options: MutationOptions = {},
86
+ ): Promise<FleetOperation> {
87
+ const res = await this.http.request<OperationEnvelope>({
88
+ method: "POST",
89
+ path: `/api/fleet/operations/${encodeURIComponent(id)}/${action}`,
90
+ idempotencyKey: options.idempotencyKey,
91
+ body,
92
+ });
93
+ return res.operation;
94
+ }
95
+ }
96
+
97
+ export class FleetRolloutsResource {
98
+ constructor(private readonly http: HttpClient) {}
99
+
100
+ async advance(id: string, options: MutationOptions = {}): Promise<FleetRollout> {
101
+ return this.post(id, "advance", options);
102
+ }
103
+
104
+ async pause(id: string, options: MutationOptions = {}): Promise<FleetRollout> {
105
+ return this.post(id, "pause", options);
106
+ }
107
+
108
+ async resume(id: string, options: MutationOptions = {}): Promise<FleetRollout> {
109
+ return this.post(id, "resume", options);
110
+ }
111
+
112
+ // Runs the health check now instead of waiting for the next observation
113
+ // window. The rollout comes back as it was read, because the evaluation runs
114
+ // in the background.
115
+ async evaluate(id: string, options: MutationOptions = {}): Promise<FleetRollout> {
116
+ return this.post(id, "evaluate", options);
117
+ }
118
+
119
+ // Answers with the operation it queued as well as the rollout: the rollout is
120
+ // finished at that point, and the operation is the thing still worth watching.
121
+ async rollback(id: string, options: MutationOptions = {}): Promise<FleetRollbackResult> {
122
+ return this.http.request<FleetRollbackResult>({
123
+ method: "POST",
124
+ path: `/api/fleet/rollouts/${encodeURIComponent(id)}/rollback`,
125
+ idempotencyKey: options.idempotencyKey,
126
+ });
127
+ }
128
+
129
+ private async post(
130
+ id: string,
131
+ action: string,
132
+ options: MutationOptions = {},
133
+ ): Promise<FleetRollout> {
134
+ const res = await this.http.request<RolloutEnvelope>({
135
+ method: "POST",
136
+ path: `/api/fleet/rollouts/${encodeURIComponent(id)}/${action}`,
137
+ idempotencyKey: options.idempotencyKey,
138
+ });
139
+ return res.rollout;
140
+ }
141
+ }
142
+
143
+ export class FleetFindingsResource {
144
+ constructor(private readonly http: HttpClient) {}
145
+
146
+ async acknowledge(id: string, options: MutationOptions = {}): Promise<FleetAnomalyFinding> {
147
+ return this.post(id, "acknowledge", undefined, options);
148
+ }
149
+
150
+ async snooze(
151
+ id: string,
152
+ duration?: FleetSnoozeDuration,
153
+ options: MutationOptions = {},
154
+ ): Promise<FleetAnomalyFinding> {
155
+ return this.post(id, "snooze", duration === undefined ? undefined : { duration }, options);
156
+ }
157
+
158
+ async resolve(id: string, options: MutationOptions = {}): Promise<FleetAnomalyFinding> {
159
+ return this.post(id, "resolve", undefined, options);
160
+ }
161
+
162
+ async reopen(id: string, options: MutationOptions = {}): Promise<FleetAnomalyFinding> {
163
+ return this.post(id, "reopen", undefined, options);
164
+ }
165
+
166
+ async classify(
167
+ id: string,
168
+ classification: string,
169
+ options: MutationOptions = {},
170
+ ): Promise<FleetAnomalyFinding> {
171
+ return this.post(id, "classify", { classification }, options);
172
+ }
173
+
174
+ // Omit `membershipId`, or pass null, to unassign. The wire value for "no
175
+ // owner" is null, which is what the spec declares; the server treats any
176
+ // blank as unassign, but sending null keeps the client honest.
177
+ async assign(
178
+ id: string,
179
+ membershipId?: number | string | null,
180
+ options: MutationOptions = {},
181
+ ): Promise<FleetAnomalyFinding> {
182
+ return this.post(id, "assign", { membership_id: membershipId ?? null }, options);
183
+ }
184
+
185
+ // Raises the finding to an incident so it leaves the routine triage queue.
186
+ async promote(id: string, options: MutationOptions = {}): Promise<FleetAnomalyFinding> {
187
+ return this.post(id, "promote", undefined, options);
188
+ }
189
+
190
+ // Sets the runbook link. Pass null to clear it; the server also accepts an
191
+ // empty string, which is what the CLI flag sends.
192
+ async updateRunbook(
193
+ id: string,
194
+ runbookUrl: string | null,
195
+ options: MutationOptions = {},
196
+ ): Promise<FleetAnomalyFinding> {
197
+ const res = await this.http.request<FindingEnvelope>({
198
+ method: "PATCH",
199
+ path: `/api/fleet/findings/${encodeURIComponent(id)}`,
200
+ idempotencyKey: options.idempotencyKey,
201
+ body: { runbook_url: runbookUrl },
202
+ });
203
+ return res.finding;
204
+ }
205
+
206
+ private async post(
207
+ id: string,
208
+ action: string,
209
+ body?: unknown,
210
+ options: MutationOptions = {},
211
+ ): Promise<FleetAnomalyFinding> {
212
+ const res = await this.http.request<FindingEnvelope>({
213
+ method: "POST",
214
+ path: `/api/fleet/findings/${encodeURIComponent(id)}/${action}`,
215
+ idempotencyKey: options.idempotencyKey,
216
+ body,
217
+ });
218
+ return res.finding;
219
+ }
220
+ }
221
+
222
+ export class FleetSavedViewsResource {
223
+ constructor(private readonly http: HttpClient) {}
224
+
225
+ async create(
226
+ params: FleetSavedViewParams,
227
+ options: MutationOptions = {},
228
+ ): Promise<FleetSavedView> {
229
+ const res = await this.http.request<SavedViewEnvelope>({
230
+ method: "POST",
231
+ path: "/api/fleet/saved_views",
232
+ idempotencyKey: options.idempotencyKey,
233
+ body: { saved_view: params },
234
+ });
235
+ return res.saved_view;
236
+ }
237
+
238
+ async update(
239
+ id: string,
240
+ params: Partial<FleetSavedViewParams>,
241
+ options: MutationOptions = {},
242
+ ): Promise<FleetSavedView> {
243
+ const res = await this.http.request<SavedViewEnvelope>({
244
+ method: "PATCH",
245
+ path: `/api/fleet/saved_views/${encodeURIComponent(id)}`,
246
+ idempotencyKey: options.idempotencyKey,
247
+ body: { saved_view: params },
248
+ });
249
+ return res.saved_view;
250
+ }
251
+
252
+ async delete(id: string, options: MutationOptions = {}): Promise<void> {
253
+ await this.http.request<void>({
254
+ method: "DELETE",
255
+ path: `/api/fleet/saved_views/${encodeURIComponent(id)}`,
256
+ idempotencyKey: options.idempotencyKey,
257
+ });
258
+ }
259
+
260
+ // Favorites are per membership, so this marks the view for the person who
261
+ // created the API key, not for the organization.
262
+ async favorite(id: string, options: MutationOptions = {}): Promise<void> {
263
+ await this.http.request<void>({
264
+ method: "POST",
265
+ path: `/api/fleet/saved_views/${encodeURIComponent(id)}/favorite`,
266
+ idempotencyKey: options.idempotencyKey,
267
+ });
268
+ }
269
+
270
+ async unfavorite(id: string, options: MutationOptions = {}): Promise<void> {
271
+ await this.http.request<void>({
272
+ method: "DELETE",
273
+ path: `/api/fleet/saved_views/${encodeURIComponent(id)}/favorite`,
274
+ idempotencyKey: options.idempotencyKey,
275
+ });
276
+ }
277
+ }
278
+
279
+ export class FleetAnalyticsResource {
280
+ constructor(private readonly http: HttpClient) {}
281
+
282
+ // Stores an explorer chart as a private saved view pinned to the caller. The
283
+ // parameters are the same ones the console's chart URL carries.
284
+ async save(
285
+ params: FleetAnalyticsSaveParams,
286
+ options: MutationOptions = {},
287
+ ): Promise<FleetSavedView> {
288
+ const res = await this.http.request<SavedViewEnvelope>({
289
+ method: "POST",
290
+ path: "/api/fleet/analytics/saved_views",
291
+ idempotencyKey: options.idempotencyKey,
292
+ body: params,
293
+ });
294
+ return res.saved_view;
295
+ }
296
+
297
+ // Removes the chart from the caller's pinned list. The view itself survives.
298
+ async unpin(id: string, options: MutationOptions = {}): Promise<void> {
299
+ await this.http.request<void>({
300
+ method: "DELETE",
301
+ path: `/api/fleet/analytics/saved_views/${encodeURIComponent(id)}/pin`,
302
+ idempotencyKey: options.idempotencyKey,
303
+ });
304
+ }
305
+ }
306
+
307
+ export class FleetExportsResource {
308
+ constructor(private readonly http: HttpClient) {}
309
+
310
+ // Returns the CSV body as a string. `users` and `sessions` exports carry
311
+ // personal data, so they need a membership that may read sessions.
312
+ async create(params: FleetExportParams, options: MutationOptions = {}): Promise<string> {
313
+ return this.http.request<string>({
314
+ method: "POST",
315
+ path: "/api/fleet/exports",
316
+ headers: { Accept: "text/csv" },
317
+ idempotencyKey: options.idempotencyKey,
318
+ body: { export: params },
319
+ });
320
+ }
321
+ }
322
+
323
+ export class FleetResource {
324
+ readonly operations: FleetOperationsResource;
325
+ readonly rollouts: FleetRolloutsResource;
326
+ readonly findings: FleetFindingsResource;
327
+ readonly savedViews: FleetSavedViewsResource;
328
+ readonly analytics: FleetAnalyticsResource;
329
+ readonly exports: FleetExportsResource;
330
+
331
+ constructor(http: HttpClient) {
332
+ this.operations = new FleetOperationsResource(http);
333
+ this.rollouts = new FleetRolloutsResource(http);
334
+ this.findings = new FleetFindingsResource(http);
335
+ this.savedViews = new FleetSavedViewsResource(http);
336
+ this.analytics = new FleetAnalyticsResource(http);
337
+ this.exports = new FleetExportsResource(http);
338
+ }
339
+ }
@@ -1,7 +1,8 @@
1
- import { errorFor } from "./errors.js";
1
+ import { errorFor, parseRetryAfterSeconds } from "./errors.js";
2
+ import { assertIdempotencyKey, randomIdempotencyKey } from "./idempotency.js";
2
3
  import type { ErrorBody } from "./types.js";
3
4
 
4
- export interface KartaOptions {
5
+ export interface DhartaOptions {
5
6
  // Bearer token, e.g. "dh_live_…". Required for headless / SDK use.
6
7
  apiKey: string;
7
8
  baseUrl?: string;
@@ -14,6 +15,18 @@ export interface KartaOptions {
14
15
  // Map a transport failure (DNS, refused connection, TLS, etc.) to the Error
15
16
  // to throw. Defaults to a generic SDK transport message.
16
17
  formatNetworkError?: (url: string, error: unknown) => Error;
18
+ // The Idempotency-Key for every request this client makes that does not name
19
+ // one of its own. Set it when the CALLER, not the library, owns recovery: a
20
+ // CI job that reruns after an ambiguous failure needs the second run to
21
+ // present the key the first run used, and it cannot do that if each process
22
+ // mints its own. This is what `dharta --idempotency-key` wires up.
23
+ //
24
+ // It fills in, it does not overrule. A request that names its own key keeps
25
+ // it, which is what lets one command make two writes under one caller key:
26
+ // it derives a key per write and each survives. Without that, the second
27
+ // write would present the first one's key with a different body and come
28
+ // back 422.
29
+ idempotencyKey?: string;
17
30
  }
18
31
 
19
32
  export interface RequestOptions {
@@ -23,8 +36,23 @@ export interface RequestOptions {
23
36
  body?: unknown;
24
37
  // Extra headers, merged over the Authorization/Accept/Content-Type defaults.
25
38
  headers?: Record<string, string>;
39
+ // The Idempotency-Key for THIS request. Set it when the value belongs to the
40
+ // operation rather than to the client: a command that writes twice derives
41
+ // one key per write and passes each here. It outranks the client-wide key.
42
+ //
43
+ // Leave it undefined and a mutation still carries a key: the client fills in
44
+ // its own, or mints a UUID. Minting lives here, in one place, so the
45
+ // client-wide key has somewhere to apply. A resource method that minted its
46
+ // own would leave nothing for the caller's key to fill in.
47
+ idempotencyKey?: string;
26
48
  }
27
49
 
50
+ const IDEMPOTENCY_HEADER = "Idempotency-Key";
51
+
52
+ // The methods the platform installs its idempotency filter on. A read carries
53
+ // the header only when the caller put it there, and never a minted one.
54
+ const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
55
+
28
56
  const PROTECTED_HEADER_NAMES = new Set([
29
57
  "authorization",
30
58
  "host",
@@ -41,8 +69,9 @@ export class HttpClient {
41
69
  private readonly fetchImpl: typeof fetch;
42
70
  private readonly formatError?: (status: number, body: unknown) => Error;
43
71
  private readonly formatNetworkError?: (url: string, error: unknown) => Error;
72
+ private readonly idempotencyKey?: string;
44
73
 
45
- constructor(opts: KartaOptions) {
74
+ constructor(opts: DhartaOptions) {
46
75
  if (!opts.apiKey) {
47
76
  throw new Error("DhartaSDK: `apiKey` is required.");
48
77
  }
@@ -60,6 +89,12 @@ export class HttpClient {
60
89
  this.fetchImpl = f.bind(globalThis);
61
90
  this.formatError = opts.formatError;
62
91
  this.formatNetworkError = opts.formatNetworkError;
92
+ // Checked here rather than at first use, so a bad client-wide key fails
93
+ // where it was set instead of on whichever request happens to go first.
94
+ this.idempotencyKey =
95
+ opts.idempotencyKey === undefined
96
+ ? undefined
97
+ : assertIdempotencyKey(opts.idempotencyKey, "idempotencyKey");
63
98
  }
64
99
 
65
100
  async request<T>(opts: RequestOptions): Promise<T> {
@@ -71,11 +106,15 @@ export class HttpClient {
71
106
  }
72
107
 
73
108
  const extraHeaders = sanitizeHeaders(opts.headers);
74
- const headers: Record<string, string> = {
75
- Authorization: `Bearer ${this.apiKey}`,
76
- Accept: "application/json",
77
- ...extraHeaders,
78
- };
109
+ const headers: Record<string, string> = this.withIdempotencyKey(
110
+ {
111
+ Authorization: `Bearer ${this.apiKey}`,
112
+ Accept: "application/json",
113
+ ...extraHeaders,
114
+ },
115
+ opts.method ?? "GET",
116
+ opts.idempotencyKey,
117
+ );
79
118
  let body: string | undefined;
80
119
  if (opts.body !== undefined) {
81
120
  headers["Content-Type"] = "application/json";
@@ -108,9 +147,21 @@ export class HttpClient {
108
147
  }
109
148
 
110
149
  if (!res.ok) {
111
- throw this.formatError
150
+ const retryAfterSeconds = parseRetryAfterSeconds(res.headers.get("Retry-After"));
151
+ const error = this.formatError
112
152
  ? this.formatError(res.status, parsed)
113
- : errorFor(res.status, parsed as ErrorBody | string | null);
153
+ : errorFor(res.status, parsed as ErrorBody | string | null, retryAfterSeconds);
154
+ // A caller-supplied `formatError` builds its own Error and never sees
155
+ // the response, so attach the delay here too. Otherwise the CLI, which
156
+ // supplies one, would print "you are rate limited" and drop the only
157
+ // part of the answer the user needs: how long to wait.
158
+ if (retryAfterSeconds !== undefined && !("retryAfterSeconds" in error)) {
159
+ Object.defineProperty(error, "retryAfterSeconds", {
160
+ value: retryAfterSeconds,
161
+ enumerable: true,
162
+ });
163
+ }
164
+ throw error;
114
165
  }
115
166
  return parsed as T;
116
167
  }
@@ -120,19 +171,61 @@ export class HttpClient {
120
171
  async raw(path: string, init?: RequestInit): Promise<Response> {
121
172
  const extraHeaders = sanitizeHeaders(init?.headers);
122
173
  const url = this.baseUrl + path;
174
+ // Built OUTSIDE the try. It rejects a malformed Idempotency-Key, and a
175
+ // rejected value is not a transport failure: catching it here would report
176
+ // "could not reach the host" for a key the caller mistyped.
177
+ const headers = this.withIdempotencyKey(
178
+ {
179
+ Authorization: `Bearer ${this.apiKey}`,
180
+ ...extraHeaders,
181
+ },
182
+ init?.method ?? "GET",
183
+ );
123
184
  try {
124
- return await this.fetchImpl(url, {
125
- ...init,
126
- headers: {
127
- Authorization: `Bearer ${this.apiKey}`,
128
- ...extraHeaders,
129
- },
130
- });
185
+ return await this.fetchImpl(url, { ...init, headers });
131
186
  } catch (error) {
132
187
  throw this.networkError(url, error);
133
188
  }
134
189
  }
135
190
 
191
+ // One place decides the Idempotency-Key, most specific first: the key this
192
+ // request named, then one a caller wrote as a header by hand, then the
193
+ // client-wide key, then a fresh UUID on a mutation. A read that named none of
194
+ // them carries no key, which is what it did before minting moved here.
195
+ //
196
+ // Whatever wins is written back under the canonical spelling and every other
197
+ // spelling is dropped. HTTP header names are case-insensitive, so leaving a
198
+ // lowercase `idempotency-key` beside the winner would send the value twice
199
+ // and let the server choose.
200
+ private withIdempotencyKey(
201
+ headers: Record<string, string>,
202
+ method: string,
203
+ requestKey?: string,
204
+ ): Record<string, string> {
205
+ const merged: Record<string, string> = {};
206
+ let handWritten: string | undefined;
207
+ for (const [key, value] of Object.entries(headers)) {
208
+ if (key.toLowerCase() === IDEMPOTENCY_HEADER.toLowerCase()) {
209
+ handWritten = value;
210
+ continue;
211
+ }
212
+ merged[key] = value;
213
+ }
214
+
215
+ // Both caller-supplied sources are checked, not only the winner: a key that
216
+ // is wrong is worth saying so about, and a caller who supplied two would
217
+ // otherwise learn about the loser's mistake only after changing the winner.
218
+ if (requestKey !== undefined) assertIdempotencyKey(requestKey, "idempotencyKey");
219
+ if (handWritten !== undefined) assertIdempotencyKey(handWritten, "the Idempotency-Key header");
220
+
221
+ const chosen = requestKey
222
+ ?? handWritten
223
+ ?? this.idempotencyKey
224
+ ?? (MUTATING_METHODS.has(method.toUpperCase()) ? randomIdempotencyKey() : undefined);
225
+ if (chosen !== undefined) merged[IDEMPOTENCY_HEADER] = chosen;
226
+ return merged;
227
+ }
228
+
136
229
  private networkError(url: string, error: unknown): Error {
137
230
  if (this.formatNetworkError) return this.formatNetworkError(url, error);
138
231
  return new Error(`DhartaSDK: could not reach ${new URL(url).origin} (${formatCause(error)}).`);