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.
- package/README.md +380 -63
- package/package.json +5 -1
- package/skills/requestshield/SKILL.md +1 -1
- package/skills/requestshield/references/cli.md +7 -6
- package/src/agent-detector.mjs +37 -9
- package/src/api-client.mjs +100 -5
- package/src/args.mjs +111 -2
- package/src/cli.mjs +233 -18
- package/src/commands/apps-get.mjs +64 -0
- package/src/commands/apps-list.mjs +90 -0
- package/src/commands/billing-get.mjs +110 -0
- package/src/commands/challenge-volume.mjs +81 -0
- package/src/commands/contract.mjs +106 -0
- package/src/commands/update-check.mjs +139 -0
- package/src/config.mjs +8 -0
package/src/api-client.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 {{
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|