requestshield 0.1.3 → 0.1.5

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.
@@ -2,6 +2,14 @@
2
2
 
3
3
  import { CliError } from "./errors.mjs";
4
4
 
5
+ const API_ENDPOINTS = {
6
+ SIGNIN: "/v1/cli/signin",
7
+ SIGNIN_TOKEN: "/v1/cli/signin/token",
8
+ KEYS_ROTATE: "/v1/cli/keys/rotate",
9
+ INTEGRATION_CONTRACT: "/v1/integration-contract",
10
+ APPLICATIONS: "/v1/applications",
11
+ };
12
+
5
13
  export class ManagementApiClient {
6
14
  /** @param {{ baseUrl: string, fetchImpl?: typeof fetch }} options */
7
15
  constructor({ baseUrl, fetchImpl = fetch }) {
@@ -21,7 +29,7 @@ export class ManagementApiClient {
21
29
  }
22
30
 
23
31
  async startSignin() {
24
- return this.#request("/v1/cli/signin", {
32
+ return this.#request(API_ENDPOINTS.SIGNIN, {
25
33
  method: "POST",
26
34
  body: JSON.stringify({ client: "requestshield-cli" }),
27
35
  });
@@ -30,7 +38,7 @@ export class ManagementApiClient {
30
38
  /** @param {string} deviceCode */
31
39
  async pollSignin(deviceCode) {
32
40
  return this.#request(
33
- "/v1/cli/signin/token",
41
+ API_ENDPOINTS.SIGNIN_TOKEN,
34
42
  { method: "POST", body: JSON.stringify({ deviceCode }) },
35
43
  true,
36
44
  );
@@ -38,13 +46,97 @@ export class ManagementApiClient {
38
46
 
39
47
  /** @param {string} accessToken */
40
48
  async rotateKeys(accessToken) {
41
- return this.#request("/v1/cli/keys/rotate", {
49
+ return this.#request(API_ENDPOINTS.KEYS_ROTATE, {
42
50
  method: "POST",
43
51
  headers: { Authorization: `Bearer ${accessToken}` },
44
52
  body: "{}",
45
53
  });
46
54
  }
47
55
 
56
+ /** @param {string} accessToken */
57
+ async integrationContract(accessToken) {
58
+ return this.#request(API_ENDPOINTS.INTEGRATION_CONTRACT, {
59
+ method: "GET",
60
+ headers: {
61
+ Accept: "application/json",
62
+ Authorization: `Bearer ${accessToken}`,
63
+ },
64
+ });
65
+ }
66
+
67
+ /** @param {string} accessToken */
68
+ async listApps(accessToken) {
69
+ return this.#request(API_ENDPOINTS.APPLICATIONS, {
70
+ method: "GET",
71
+ headers: {
72
+ Accept: "application/json",
73
+ Authorization: `Bearer ${accessToken}`,
74
+ },
75
+ });
76
+ }
77
+
78
+ /** @param {string} accessToken @param {string} appKey */
79
+ async getApp(accessToken, appKey) {
80
+ return this.#request(
81
+ `${API_ENDPOINTS.APPLICATIONS}/${encodeURIComponent(appKey)}`,
82
+ {
83
+ method: "GET",
84
+ headers: {
85
+ Accept: "application/json",
86
+ Authorization: `Bearer ${accessToken}`,
87
+ },
88
+ },
89
+ );
90
+ }
91
+
92
+ /**
93
+ * @param {string} accessToken
94
+ * @param {string} appKey
95
+ * @param {{ from?: string, to?: string, granularity?: string }} options
96
+ */
97
+ async challengeVolume(accessToken, appKey, options) {
98
+ const query = new URLSearchParams();
99
+
100
+ if (options.from)
101
+ query.set("from", options.from);
102
+
103
+ if (options.to)
104
+ query.set("to", options.to);
105
+
106
+ if (options.granularity)
107
+ query.set("granularity", options.granularity);
108
+
109
+ const suffix = query.size > 0 ? `?${query}` : "";
110
+
111
+ const endpoint =
112
+ `${API_ENDPOINTS.APPLICATIONS}/` +
113
+ `${encodeURIComponent(appKey)}/challenge-volume` +
114
+ suffix;
115
+
116
+ return this.#request(endpoint, {
117
+ method: "GET",
118
+ headers: {
119
+ Accept: "application/json",
120
+ Authorization: `Bearer ${accessToken}`,
121
+ },
122
+ });
123
+ }
124
+
125
+
126
+ /** @param {string} accessToken @param {string} appKey */
127
+ async getBilling(accessToken, appKey) {
128
+ return this.#request(
129
+ `${API_ENDPOINTS.APPLICATIONS}/${encodeURIComponent(appKey)}/billing`,
130
+ {
131
+ method: "GET",
132
+ headers: {
133
+ Accept: "application/json",
134
+ Authorization: `Bearer ${accessToken}`,
135
+ },
136
+ },
137
+ );
138
+ }
139
+
48
140
  /**
49
141
  * @param {string} path
50
142
  * @param {RequestInit} init
@@ -77,10 +169,13 @@ export class ManagementApiClient {
77
169
  }
78
170
  }
79
171
  if (!response.ok && !allowErrorBody) {
80
- const detail = objectString(body, "message") ?? `HTTP ${response.status}`;
172
+ const nestedError = body && typeof body === "object" ? Reflect.get(body, "error") : undefined;
173
+ const detail = objectString(nestedError, "message")
174
+ ?? objectString(body, "message")
175
+ ?? `HTTP ${response.status}`;
81
176
  const exitCode = response.status === 401 || response.status === 403 ? 3 : 1;
82
177
  throw new CliError(`RequestShield API error: ${detail}`, {
83
- code: objectString(body, "code") ?? "API_ERROR",
178
+ code: objectString(nestedError, "code") ?? objectString(body, "code") ?? "API_ERROR",
84
179
  exitCode,
85
180
  });
86
181
  }
package/src/args.mjs CHANGED
@@ -2,22 +2,34 @@
2
2
 
3
3
  import { CliError } from "./errors.mjs";
4
4
 
5
- const HELP = `RequestShield customer CLI
5
+ const HELP = `RequestShield CLI
6
6
 
7
7
  Usage:
8
8
  requestshield signin
9
9
  requestshield keys create [--yes]
10
+ requestshield contract
11
+ requestshield apps list [--json]
12
+ requestshield apps get <app-key>
13
+ requestshield challenge volume <app-key> [--from <time>] [--to <time>] [--granularity <value>]
14
+ requestshield get billing <app-key>
10
15
  requestshield agent setup [--force]
11
16
  requestshield agent setup --codex [--force]
12
17
  requestshield agent setup --claude [--force]
18
+ requestshield update check
13
19
  requestshield --help
14
20
  requestshield --version`;
15
21
 
16
22
  /**
17
23
  * @typedef {{ command: "help", help: string }
18
24
  * | { command: "version" }
25
+ * | { command: "update-check" }
19
26
  * | { command: "signin" }
20
27
  * | { command: "keys-create", yes: boolean }
28
+ * | { command: "contract" }
29
+ * | { command: "apps-list", json: boolean }
30
+ * | { command: "apps-get", appKey: string }
31
+ * | { command: "challenge-volume", appKey: string, from?: string, to?: string, granularity?: string }
32
+ * | { command: "billing-get", appKey: string }
21
33
  * | { command: "agent-setup", agent?: "codex" | "claude", force: boolean }} ParsedArgs
22
34
  */
23
35
 
@@ -26,9 +38,13 @@ export function parseArgs(argv) {
26
38
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
27
39
  return { command: "help", help: HELP };
28
40
  }
29
- if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-V")) {
41
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V")) {
30
42
  return { command: "version" };
31
43
  }
44
+ if (argv[0] === "update" && argv[1] === "check") {
45
+ assertOnly(argv.slice(2), new Set());
46
+ return { command: "update-check" };
47
+ }
32
48
  if (argv[0] === "signin") {
33
49
  assertOnly(argv.slice(1), new Set());
34
50
  return { command: "signin" };
@@ -37,6 +53,29 @@ export function parseArgs(argv) {
37
53
  assertOnly(argv.slice(2), new Set(["--yes"]));
38
54
  return { command: "keys-create", yes: argv.includes("--yes") };
39
55
  }
56
+ if (argv[0] === "contract") {
57
+ assertOnly(argv.slice(1), new Set());
58
+ return { command: "contract" };
59
+ }
60
+ if (argv[0] === "apps" && argv[1] === "list") {
61
+ assertOnly(argv.slice(2), new Set(["--json"]));
62
+ return { command: "apps-list", json: argv.includes("--json") };
63
+ }
64
+ if (argv[0] === "apps" && argv[1] === "get") {
65
+ return {
66
+ command: "apps-get",
67
+ appKey: parseAppKey(argv, "Usage: requestshield apps get <app-key>"),
68
+ };
69
+ }
70
+ if (argv[0] === "challenge" && argv[1] === "volume") {
71
+ return parseChallengeVolume(argv);
72
+ }
73
+ if (argv[0] === "get" && argv[1] === "billing") {
74
+ return {
75
+ command: "billing-get",
76
+ appKey: parseAppKey(argv, "Usage: requestshield get billing <app-key>"),
77
+ };
78
+ }
40
79
  if (argv[0] === "agent" && argv[1] === "setup") {
41
80
  const rest = argv.slice(2);
42
81
  /** @type {"codex" | "claude" | undefined} */
@@ -76,4 +115,74 @@ function assertOnly(args, allowed) {
76
115
  }
77
116
  }
78
117
 
118
+ /** @param {string[]} argv @param {string} usage */
119
+ function parseAppKey(argv, usage) {
120
+ if (argv.length !== 3) throw new CliError(usage, { exitCode: 2 });
121
+ return validateAppKey(argv[2]);
122
+ }
123
+
124
+ /** @param {string[]} argv @returns {Extract<ParsedArgs, {command: "challenge-volume"}>} */
125
+ function parseChallengeVolume(argv) {
126
+ const usage = "Usage: requestshield challenge volume <app-key> [--from <time>] [--to <time>] [--granularity <value>]";
127
+ if (argv.length < 3) throw new CliError(usage, { exitCode: 2 });
128
+ const appKey = validateAppKey(argv[2]);
129
+ /** @type {{ from?: string, to?: string, granularity?: string }} */
130
+ const options = {};
131
+ /** @type {Map<string, "from" | "to" | "granularity">} */
132
+ const optionNames = new Map([
133
+ ["--from", "from"],
134
+ ["--to", "to"],
135
+ ["--granularity", "granularity"],
136
+ ]);
137
+ for (let index = 3; index < argv.length; index += 2) {
138
+ const option = argv[index];
139
+ const property = optionNames.get(option);
140
+ if (!property) throw new CliError(`Unknown option: ${option}`, { exitCode: 2 });
141
+ const value = argv[index + 1];
142
+ if (!value || value.startsWith("--")) {
143
+ throw new CliError(`${option} requires a value`, { exitCode: 2 });
144
+ }
145
+ if (options[property] !== undefined) {
146
+ throw new CliError(`${option} may only be provided once`, { exitCode: 2 });
147
+ }
148
+ if ((property === "from" || property === "to") && !isIsoTime(value)) {
149
+ throw new CliError(`${option} must be an ISO-8601 timestamp`, {
150
+ code: "INVALID_TIME",
151
+ exitCode: 2,
152
+ });
153
+ }
154
+ if (property === "granularity" && !/^[a-z][a-z0-9_-]{0,31}$/.test(value)) {
155
+ throw new CliError(
156
+ "--granularity must be 1-32 lowercase letters, numbers, '_' or '-'",
157
+ { code: "INVALID_GRANULARITY", exitCode: 2 },
158
+ );
159
+ }
160
+ options[property] = value;
161
+ }
162
+ if (options.from && options.to && Date.parse(options.from) > Date.parse(options.to)) {
163
+ throw new CliError("--from must not be later than --to", {
164
+ code: "INVALID_TIME_RANGE",
165
+ exitCode: 2,
166
+ });
167
+ }
168
+ return { command: "challenge-volume", appKey, ...options };
169
+ }
170
+
171
+ /** @param {string} appKey */
172
+ function validateAppKey(appKey) {
173
+ if (!/^[A-Za-z0-9._~-]{1,128}$/.test(appKey)) {
174
+ throw new CliError(
175
+ "App Key must be 1-128 characters using letters, numbers, '.', '_', '~', or '-'",
176
+ { code: "INVALID_APP_KEY", exitCode: 2 },
177
+ );
178
+ }
179
+ return appKey;
180
+ }
181
+
182
+ /** @param {string} value */
183
+ function isIsoTime(value) {
184
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
185
+ && Number.isFinite(Date.parse(value));
186
+ }
187
+
79
188
  export { HELP };
package/src/cli.mjs CHANGED
@@ -4,37 +4,252 @@
4
4
  import { parseArgs } from "./args.mjs";
5
5
  import { ManagementApiClient } from "./api-client.mjs";
6
6
  import { SessionStore } from "./session-store.mjs";
7
+
7
8
  import { signin } from "./commands/signin.mjs";
8
9
  import { createKeys } from "./commands/keys-create.mjs";
10
+ import { showIntegrationContract } from "./commands/contract.mjs";
11
+ import { listApps } from "./commands/apps-list.mjs";
12
+ import { getApp } from "./commands/apps-get.mjs";
13
+ import { showChallengeVolume } from "./commands/challenge-volume.mjs";
14
+ import { showBilling } from "./commands/billing-get.mjs";
9
15
  import { setupAgent } from "./commands/agent-setup.mjs";
16
+ import { checkForUpdate } from "./commands/update-check.mjs";
17
+
10
18
  import packageJson from "../package.json" with { type: "json" };
19
+ import { DEFAULT_MANAGEMENT_API_URL } from "./config.mjs";
20
+
21
+ /**
22
+ * @typedef {{
23
+ * log: (message: string) => void,
24
+ * api?: ManagementApiClient,
25
+ * sessions?: SessionStore,
26
+ * env?: NodeJS.ProcessEnv,
27
+ * homeDir?: string,
28
+ * sourceDir?: string,
29
+ * executablePath?: string,
30
+ * detectAgents?: () => Promise<Array<"codex" | "claude">>,
31
+ * selectAgent?: (
32
+ * detected: Array<"codex" | "claude">
33
+ * ) => Promise<"codex" | "claude">,
34
+ * wait?: (milliseconds: number) => Promise<unknown>,
35
+ * confirm?: () => Promise<boolean>,
36
+ * fetchImpl?: typeof fetch,
37
+ * confirmUpdate?: () => Promise<boolean>,
38
+ * installLatest?: (
39
+ * packageName: string,
40
+ * version: string
41
+ * ) => Promise<void>
42
+ * }} CommandContext
43
+ */
44
+
45
+ /**
46
+ * @typedef {CommandContext & {
47
+ * api: ManagementApiClient,
48
+ * sessions: SessionStore
49
+ * }} AuthenticatedCommandContext
50
+ */
51
+
52
+ /**
53
+ * @typedef {{
54
+ * requiresApiSession: boolean,
55
+ * handler: (
56
+ * parsed: any,
57
+ * context: CommandContext
58
+ * ) => unknown | Promise<unknown>
59
+ * }} CommandHandler
60
+ */
61
+
62
+ /**
63
+ * Put every CLI command handler here.
64
+ *
65
+ * @type {Record<string, CommandHandler>}
66
+ */
67
+ const COMMAND_HANDLERS = {
68
+ "help": {
69
+ requiresApiSession: false,
70
+ handler: (parsed, context) =>
71
+ context.log(parsed.help),
72
+ },
73
+
74
+ "version": {
75
+ requiresApiSession: false,
76
+ handler: (_parsed, context) =>
77
+ context.log(`requestshield ${packageJson.version}`),
78
+ },
79
+
80
+ "update-check": {
81
+ requiresApiSession: false,
82
+ handler: (_parsed, context) =>
83
+ checkForUpdate({
84
+ currentVersion: packageJson.version,
85
+ packageName: packageJson.name,
86
+ log: context.log,
87
+ fetchImpl: context.fetchImpl,
88
+ confirm: context.confirmUpdate,
89
+ install: context.installLatest,
90
+ }),
91
+ },
92
+
93
+ "agent-setup": {
94
+ requiresApiSession: false,
95
+ handler: (parsed, context) =>
96
+ setupAgent(parsed, context),
97
+ },
98
+
99
+ "signin": {
100
+ requiresApiSession: true,
101
+ handler: (_parsed, context) =>
102
+ signin(requireApiSession(context)),
103
+ },
104
+
105
+ "keys-create": {
106
+ requiresApiSession: true,
107
+ handler: (parsed, context) =>
108
+ createKeys(parsed, requireApiSession(context)),
109
+ },
110
+
111
+ "contract": {
112
+ requiresApiSession: true,
113
+ handler: (_parsed, context) =>
114
+ showIntegrationContract(requireApiSession(context)),
115
+ },
116
+
117
+ "apps-list": {
118
+ requiresApiSession: true,
119
+ handler: (parsed, context) =>
120
+ listApps(parsed, requireApiSession(context)),
121
+ },
122
+
123
+ "apps-get": {
124
+ requiresApiSession: true,
125
+ handler: (parsed, context) =>
126
+ getApp(parsed, requireApiSession(context)),
127
+ },
128
+
129
+ "challenge-volume": {
130
+ requiresApiSession: true,
131
+ handler: (parsed, context) =>
132
+ showChallengeVolume(parsed, requireApiSession(context)),
133
+ },
134
+
135
+ "billing-get": {
136
+ requiresApiSession: true,
137
+ handler: (parsed, context) =>
138
+ showBilling(parsed, requireApiSession(context)),
139
+ },
140
+ };
141
+
142
+ /**
143
+ * Narrow the shared context before an authenticated handler can use it.
144
+ *
145
+ * @param {CommandContext} context
146
+ * @returns {AuthenticatedCommandContext}
147
+ */
148
+ function requireApiSession(context) {
149
+ if (!context.api || !context.sessions) {
150
+ throw new Error("Authenticated command context was not initialized");
151
+ }
152
+
153
+ return {
154
+ ...context,
155
+ api: context.api,
156
+ sessions: context.sessions,
157
+ };
158
+ }
11
159
 
12
160
  /**
13
161
  * @param {string[]} argv
14
- * @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude">, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean> }} [deps]
162
+ * @param {{
163
+ * log?: (message: string) => void,
164
+ * api?: ManagementApiClient,
165
+ * sessions?: SessionStore,
166
+ * env?: NodeJS.ProcessEnv,
167
+ * homeDir?: string,
168
+ * sourceDir?: string,
169
+ * executablePath?: string,
170
+ * detectAgents?: () => Promise<Array<"codex" | "claude">>,
171
+ * selectAgent?: (
172
+ * detected: Array<"codex" | "claude">
173
+ * ) => Promise<"codex" | "claude">,
174
+ * wait?: (milliseconds: number) => Promise<unknown>,
175
+ * confirm?: () => Promise<boolean>,
176
+ * fetchImpl?: typeof fetch,
177
+ * confirmUpdate?: () => Promise<boolean>,
178
+ * installLatest?: (
179
+ * packageName: string,
180
+ * version: string
181
+ * ) => Promise<void>
182
+ * }} [deps]
15
183
  */
16
184
  export async function run(argv, deps = {}) {
17
185
  const parsed = parseArgs(argv);
18
- const log = deps.log ?? ((message) => console.log(message));
19
- if (parsed.command === "help") return log(parsed.help);
20
- if (parsed.command === "version") return log(packageJson.version);
21
186
 
22
- if (parsed.command === "agent-setup") {
23
- return setupAgent(parsed, { log, ...deps });
187
+ const log =
188
+ deps.log ??
189
+ ((message) => console.log(message));
190
+
191
+ /*
192
+ * Find the requested command.
193
+ */
194
+ const command = COMMAND_HANDLERS[parsed.command];
195
+
196
+ if (!command) {
197
+ throw new Error(
198
+ `Unsupported command: ${String(parsed.command)}`
199
+ );
24
200
  }
25
201
 
26
- const env = deps.env ?? process.env;
202
+ /*
203
+ * Dependencies available to every command.
204
+ */
205
+ /** @type {CommandContext} */
206
+ const context = {
207
+ ...deps,
208
+ log,
209
+ };
210
+
211
+ /*
212
+ * Only initialize the Management API and session
213
+ * for commands that require them.
214
+ */
215
+ if (command.requiresApiSession) {
216
+ const env =
217
+ deps.env ??
218
+ process.env;
27
219
 
28
- const api = deps.api ?? new ManagementApiClient({
29
- baseUrl:
30
- env.REQUESTSHIELD_API_URL ??
31
- "https://api.intellifend.ai",
32
- });
220
+ const api =
221
+ deps.api ??
222
+ new ManagementApiClient({
223
+ baseUrl:
224
+ env.REQUESTSHIELD_API_URL ??
225
+ DEFAULT_MANAGEMENT_API_URL,
226
+ });
227
+
228
+ const sessions =
229
+ deps.sessions ??
230
+ new SessionStore({
231
+ env,
232
+ homeDir: deps.homeDir,
233
+ });
234
+
235
+ const authenticatedContext = {
236
+ ...context,
237
+ env,
238
+ api,
239
+ sessions,
240
+ };
241
+
242
+ return command.handler(
243
+ parsed,
244
+ authenticatedContext
245
+ );
246
+ }
33
247
 
34
- const sessions = deps.sessions ?? new SessionStore({
35
- env,
36
- homeDir: deps.homeDir,
37
- });
38
- if (parsed.command === "signin") return signin({ api, sessions, log, ...deps });
39
- if (parsed.command === "keys-create") return createKeys(parsed, { api, sessions, log, ...deps });
248
+ /*
249
+ * Execute commands that do not need an API session.
250
+ */
251
+ return command.handler(
252
+ parsed,
253
+ context
254
+ );
40
255
  }
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+
3
+ import { CliError } from "../errors.mjs";
4
+ import { objectString } from "../api-client.mjs";
5
+
6
+ const APPLICATION_STATUSES = new Set(["ready", "active", "deactivated"]);
7
+
8
+ /**
9
+ * @param {{ appKey: string }} options
10
+ * @param {{ api: { getApp(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
11
+ */
12
+ export async function getApp(options, deps) {
13
+ const accessToken =
14
+ await deps.sessions.loadToken();
15
+
16
+ const result =
17
+ await deps.api.getApp(accessToken, options.appKey);
18
+
19
+ const app =
20
+ parseApp(result.body, options.appKey);
21
+
22
+ deps.log(JSON.stringify({ ok: true, data: app }, null, 2));
23
+ }
24
+
25
+ /** @param {unknown} body @param {string} requestedAppKey */
26
+ function parseApp(body, requestedAppKey) {
27
+ const ok = body && typeof body === "object" ?
28
+ Reflect.get(body, "ok") : undefined;
29
+
30
+ const data = body && typeof body === "object" ?
31
+ Reflect.get(body, "data") : undefined;
32
+
33
+ if (ok !== true || !data || typeof data !== "object" || Array.isArray(data)) {
34
+ throw invalidResponse(
35
+ "The application response did not contain ok=true and data"
36
+ );
37
+ }
38
+
39
+ const appKey = objectString(data, "app_key");
40
+ const appName = objectString(data, "name");
41
+ const status = objectString(data, "status");
42
+
43
+ if (appKey !== requestedAppKey) {
44
+ throw invalidResponse("The application response did not match the requested App Key");
45
+ }
46
+
47
+ if (!appName) {
48
+ throw invalidResponse("The application response did not contain name");
49
+ }
50
+
51
+ if (!status || !APPLICATION_STATUSES.has(status)) {
52
+ throw invalidResponse(
53
+ "The application response status must be ready, active, or deactivated",
54
+ );
55
+ }
56
+
57
+ // Construct a new object so unexpected response fields, especially secrets, cannot be printed.
58
+ return { app_key: appKey, name: appName, status };
59
+ }
60
+
61
+ /** @param {string} message */
62
+ function invalidResponse(message) {
63
+ return new CliError(message, { code: "INVALID_RESPONSE" });
64
+ }