@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.
- package/LICENSE +1 -1
- package/README.md +215 -11
- package/dist/agent.js +3 -2
- package/dist/agent.js.map +1 -1
- package/dist/api-error.js +6 -6
- package/dist/api-error.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +860 -149
- package/dist/cli.js.map +1 -1
- package/dist/command-tree.d.ts +3 -0
- package/dist/command-tree.js +10 -0
- package/dist/command-tree.js.map +1 -0
- package/dist/commands/agents.d.ts +54 -17
- package/dist/commands/agents.js +246 -136
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/analytics.js +6 -5
- package/dist/commands/analytics.js.map +1 -1
- package/dist/commands/api.js +71 -86
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/appearance.js +5 -6
- package/dist/commands/appearance.js.map +1 -1
- package/dist/commands/artifact.js +7 -6
- package/dist/commands/artifact.js.map +1 -1
- package/dist/commands/audit-log.js +2 -1
- package/dist/commands/audit-log.js.map +1 -1
- package/dist/commands/auth.js +26 -6
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/caps.js +11 -15
- package/dist/commands/caps.js.map +1 -1
- package/dist/commands/deploy.js +25 -10
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +10 -1
- package/dist/commands/dev.js +56 -25
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +60 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/embed-keys.js +4 -13
- package/dist/commands/embed-keys.js.map +1 -1
- package/dist/commands/env.js +4 -7
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/files.js +3 -4
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/fleet.d.ts +129 -0
- package/dist/commands/fleet.js +408 -0
- package/dist/commands/fleet.js.map +1 -0
- package/dist/commands/genui.d.ts +2 -0
- package/dist/commands/genui.js +53 -0
- package/dist/commands/genui.js.map +1 -0
- package/dist/commands/git-credential.js +2 -3
- package/dist/commands/git-credential.js.map +1 -1
- package/dist/commands/kartas.d.ts +9 -0
- package/dist/commands/kartas.js +33 -11
- package/dist/commands/kartas.js.map +1 -1
- package/dist/commands/keys.d.ts +17 -18
- package/dist/commands/keys.js +108 -19
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/login.js +2 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/members.d.ts +10 -0
- package/dist/commands/members.js +20 -6
- package/dist/commands/members.js.map +1 -1
- package/dist/commands/model-connections.d.ts +44 -0
- package/dist/commands/model-connections.js +435 -0
- package/dist/commands/model-connections.js.map +1 -0
- package/dist/commands/open.d.ts +2 -2
- package/dist/commands/open.js +4 -3
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/org.d.ts +9 -0
- package/dist/commands/org.js +42 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/organization.d.ts +16 -0
- package/dist/commands/organization.js +118 -0
- package/dist/commands/organization.js.map +1 -0
- package/dist/commands/parity.js +1 -3
- package/dist/commands/parity.js.map +1 -1
- package/dist/commands/repos.d.ts +4 -0
- package/dist/commands/repos.js +17 -0
- package/dist/commands/repos.js.map +1 -0
- package/dist/commands/rollback.js +2 -3
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/schedules.js +26 -21
- package/dist/commands/schedules.js.map +1 -1
- package/dist/commands/scopes.js +8 -1
- package/dist/commands/scopes.js.map +1 -1
- package/dist/commands/sessions.js +9 -3
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/setup.js +5 -4
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +24 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/webhooks.d.ts +4 -1
- package/dist/commands/webhooks.js +21 -16
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/completions.js +24 -13
- package/dist/completions.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.js +121 -7
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.js +23 -7
- package/dist/context.js.map +1 -1
- package/dist/docs/cli-reference.d.ts +2 -0
- package/dist/docs/cli-reference.js +191 -0
- package/dist/docs/cli-reference.js.map +1 -0
- package/dist/exit-codes.d.ts +25 -0
- package/dist/exit-codes.js +107 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/genui.d.ts +178 -0
- package/dist/genui.js +1130 -0
- package/dist/genui.js.map +1 -0
- package/dist/harnesses.js +2 -1
- package/dist/harnesses.js.map +1 -1
- package/dist/jq.d.ts +4 -0
- package/dist/jq.js +91 -0
- package/dist/jq.js.map +1 -0
- package/dist/login-offer.js +6 -2
- package/dist/login-offer.js.map +1 -1
- package/dist/manifest.d.ts +12 -0
- package/dist/manifest.js +126 -0
- package/dist/manifest.js.map +1 -1
- package/dist/model-auth.d.ts +29 -0
- package/dist/model-auth.js +117 -0
- package/dist/model-auth.js.map +1 -0
- package/dist/model-settings.d.ts +23 -0
- package/dist/model-settings.js +186 -0
- package/dist/model-settings.js.map +1 -0
- package/dist/money.js +2 -1
- package/dist/money.js.map +1 -1
- package/dist/output.d.ts +6 -0
- package/dist/output.js +54 -0
- package/dist/output.js.map +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/scaffold.js +2 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/validators.js +17 -11
- package/dist/validators.js.map +1 -1
- package/dist/version-check.d.ts +1 -1
- package/dist/version-check.js +1 -1
- package/dist/version-check.js.map +1 -1
- package/node_modules/@dharta/contracts/LICENSE +21 -0
- package/node_modules/@dharta/contracts/README.md +45 -0
- package/node_modules/@dharta/contracts/artifact-runtime.json +750 -0
- package/node_modules/@dharta/contracts/dist/activity.d.ts +62 -0
- package/node_modules/@dharta/contracts/dist/activity.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/activity.js +178 -0
- package/node_modules/@dharta/contracts/dist/activity.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts +20 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js +53 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts +37 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js +5107 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts +154 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts +1670 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js +1903 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.js +377 -0
- package/node_modules/@dharta/contracts/dist/artifact-runtime.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/index.d.ts +6 -0
- package/node_modules/@dharta/contracts/dist/index.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/index.js +6 -0
- package/node_modules/@dharta/contracts/dist/index.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/model-auth.d.ts +31 -0
- package/node_modules/@dharta/contracts/dist/model-auth.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/model-auth.js +45 -0
- package/node_modules/@dharta/contracts/dist/model-auth.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/tasks.d.ts +44 -0
- package/node_modules/@dharta/contracts/dist/tasks.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/tasks.js +127 -0
- package/node_modules/@dharta/contracts/dist/tasks.js.map +1 -0
- package/node_modules/@dharta/contracts/dist/wire-events.d.ts +71 -0
- package/node_modules/@dharta/contracts/dist/wire-events.d.ts.map +1 -0
- package/node_modules/@dharta/contracts/dist/wire-events.js +48 -0
- package/node_modules/@dharta/contracts/dist/wire-events.js.map +1 -0
- package/node_modules/@dharta/contracts/model-auth-capabilities-v1.json +168 -0
- package/node_modules/@dharta/contracts/model-auth-reasons-v1.json +38 -0
- package/node_modules/@dharta/contracts/package.json +44 -0
- package/node_modules/@dharta/contracts/src/activity.ts +215 -0
- package/node_modules/@dharta/contracts/src/artifact-runtime-formats.ts +54 -0
- package/node_modules/@dharta/contracts/src/artifact-runtime-validators.generated.ts +29 -0
- package/node_modules/@dharta/contracts/src/artifact-runtime.generated.ts +2093 -0
- package/node_modules/@dharta/contracts/src/artifact-runtime.ts +520 -0
- package/node_modules/@dharta/contracts/src/index.ts +5 -0
- package/node_modules/@dharta/contracts/src/model-auth.ts +82 -0
- package/node_modules/@dharta/contracts/src/tasks.ts +142 -0
- package/node_modules/@dharta/contracts/src/wire-events.ts +150 -0
- package/node_modules/@dharta/contracts/wire-events.json +29 -0
- package/node_modules/@dharta/sdk/LICENSE +1 -1
- package/node_modules/@dharta/sdk/README.md +22 -8
- package/node_modules/@dharta/sdk/dist/errors.d.ts +9 -7
- package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -1
- package/node_modules/@dharta/sdk/dist/errors.js +36 -18
- package/node_modules/@dharta/sdk/dist/errors.js.map +1 -1
- package/node_modules/@dharta/sdk/dist/fleet.d.ts +69 -0
- package/node_modules/@dharta/sdk/dist/fleet.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/fleet.js +240 -0
- package/node_modules/@dharta/sdk/dist/fleet.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts +6 -2
- package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -1
- package/node_modules/@dharta/sdk/dist/http.js +71 -12
- package/node_modules/@dharta/sdk/dist/http.js.map +1 -1
- package/node_modules/@dharta/sdk/dist/idempotency.d.ts +12 -0
- package/node_modules/@dharta/sdk/dist/idempotency.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/idempotency.js +72 -0
- package/node_modules/@dharta/sdk/dist/idempotency.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts +20 -6
- package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -1
- package/node_modules/@dharta/sdk/dist/index.js +27 -4
- package/node_modules/@dharta/sdk/dist/index.js.map +1 -1
- package/node_modules/@dharta/sdk/dist/resources.d.ts +108 -13
- package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -1
- package/node_modules/@dharta/sdk/dist/resources.js +403 -57
- package/node_modules/@dharta/sdk/dist/resources.js.map +1 -1
- package/node_modules/@dharta/sdk/dist/types.d.ts +436 -16
- package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -1
- package/node_modules/@dharta/sdk/dist/types.js +31 -2
- package/node_modules/@dharta/sdk/dist/types.js.map +1 -1
- package/node_modules/@dharta/sdk/package.json +9 -3
- package/node_modules/@dharta/sdk/src/errors.ts +41 -18
- package/node_modules/@dharta/sdk/src/fleet.ts +339 -0
- package/node_modules/@dharta/sdk/src/http.ts +110 -17
- package/node_modules/@dharta/sdk/src/idempotency.ts +101 -0
- package/node_modules/@dharta/sdk/src/index.ts +50 -11
- package/node_modules/@dharta/sdk/src/resources.ts +549 -68
- package/node_modules/@dharta/sdk/src/types.ts +629 -20
- package/package.json +16 -6
- package/src/agent.ts +3 -2
- package/src/api-error.ts +6 -5
- package/src/cli.ts +978 -163
- package/src/command-tree.ts +13 -0
- package/src/commands/agents.ts +365 -167
- package/src/commands/analytics.ts +6 -5
- package/src/commands/api.ts +73 -92
- package/src/commands/appearance.ts +5 -6
- package/src/commands/artifact.ts +7 -6
- package/src/commands/audit-log.ts +2 -1
- package/src/commands/auth.ts +25 -5
- package/src/commands/caps.ts +11 -15
- package/src/commands/deploy.ts +18 -4
- package/src/commands/dev.ts +86 -25
- package/src/commands/doctor.ts +69 -0
- package/src/commands/embed-keys.ts +4 -14
- package/src/commands/env.ts +4 -7
- package/src/commands/files.ts +3 -4
- package/src/commands/fleet.ts +672 -0
- package/src/commands/genui.ts +59 -0
- package/src/commands/git-credential.ts +2 -3
- package/src/commands/kartas.ts +45 -11
- package/src/commands/keys.ts +152 -35
- package/src/commands/login.ts +2 -1
- package/src/commands/members.ts +31 -6
- package/src/commands/model-connections.ts +547 -0
- package/src/commands/open.ts +5 -4
- package/src/commands/org.ts +57 -0
- package/src/commands/organization.ts +153 -0
- package/src/commands/parity.ts +1 -3
- package/src/commands/repos.ts +23 -0
- package/src/commands/rollback.ts +2 -3
- package/src/commands/schedules.ts +33 -22
- package/src/commands/scopes.ts +8 -1
- package/src/commands/sessions.ts +9 -3
- package/src/commands/setup.ts +5 -4
- package/src/commands/update.ts +28 -12
- package/src/commands/webhooks.ts +30 -19
- package/src/completions.ts +25 -13
- package/src/config.ts +141 -9
- package/src/context.ts +34 -7
- package/src/docs/cli-reference.ts +206 -0
- package/src/exit-codes.ts +122 -0
- package/src/genui.ts +1398 -0
- package/src/harnesses.ts +2 -1
- package/src/jq.ts +109 -0
- package/src/login-offer.ts +6 -2
- package/src/manifest.ts +134 -0
- package/src/model-auth.ts +173 -0
- package/src/model-settings.ts +222 -0
- package/src/money.ts +5 -1
- package/src/output.ts +60 -0
- package/src/runtime.ts +1 -1
- package/src/scaffold.ts +2 -1
- package/src/validators.ts +18 -11
- package/src/version-check.ts +1 -1
- package/dist/commands/model-keys.d.ts +0 -22
- package/dist/commands/model-keys.js +0 -222
- package/dist/commands/model-keys.js.map +0 -1
- 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:
|
|
3
|
+
// Base class so consumers can `catch (e: DhartaError)` without
|
|
4
4
|
// switching on status codes manually for the common cases.
|
|
5
|
-
export class
|
|
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(
|
|
15
|
+
constructor(
|
|
16
|
+
message: string,
|
|
17
|
+
status: number,
|
|
18
|
+
body: ErrorBody | string | null,
|
|
19
|
+
retryAfterSeconds?: number,
|
|
20
|
+
) {
|
|
10
21
|
super(message);
|
|
11
|
-
this.name = "
|
|
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
|
|
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 = "
|
|
32
|
+
this.name = "DhartaAuthError";
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
export class
|
|
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 = "
|
|
39
|
+
this.name = "DhartaForbiddenError";
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
export class
|
|
43
|
+
export class DhartaNotFoundError extends DhartaError {
|
|
32
44
|
constructor(body: ErrorBody | string | null) {
|
|
33
45
|
super("Resource not found", 404, body);
|
|
34
|
-
this.name = "
|
|
46
|
+
this.name = "DhartaNotFoundError";
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
export class
|
|
50
|
+
export class DhartaValidationError extends DhartaError {
|
|
39
51
|
constructor(body: ErrorBody | string | null) {
|
|
40
52
|
super("Validation failed", 422, body);
|
|
41
|
-
this.name = "
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
if (status ===
|
|
51
|
-
if (status ===
|
|
52
|
-
if (status ===
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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)}).`);
|