requestshield 0.1.4 → 0.1.6
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 +421 -85
- package/config/.env.prod +7 -0
- package/package.json +21 -12
- package/skills/requestshield/SKILL.md +299 -307
- package/skills/requestshield/assets/AGENTS.codex.md +62 -62
- package/skills/requestshield/references/backend-java-core.md +128 -128
- package/skills/requestshield/references/backend-spring-boot.md +145 -145
- package/skills/requestshield/references/browser-manual.md +210 -210
- package/skills/requestshield/references/browser-seamless.md +156 -164
- package/skills/requestshield/references/cli.md +107 -182
- package/skills/requestshield/references/integration-planning.md +362 -389
- package/skills/requestshield/references/troubleshooting.md +114 -118
- package/src/agent-detector.mjs +102 -74
- package/src/api-client.mjs +115 -79
- package/src/args.mjs +140 -80
- package/src/browser-opener.mjs +32 -0
- package/src/cli.mjs +277 -51
- package/src/commands/agent-setup.mjs +182 -185
- package/src/commands/application-mutations.mjs +33 -0
- package/src/commands/application-response.mjs +55 -0
- package/src/commands/apps-get.mjs +20 -0
- package/src/commands/apps-list.mjs +94 -0
- package/src/commands/auth-status.mjs +37 -0
- package/src/commands/keys-create.mjs +7 -38
- package/src/commands/mutation-support.mjs +110 -0
- package/src/commands/secret-commands.mjs +45 -0
- package/src/commands/signin.mjs +70 -57
- package/src/commands/signout.mjs +9 -0
- package/src/commands/update-check.mjs +12 -4
- package/src/config.mjs +150 -0
- package/src/entrypoint.mjs +24 -0
- package/src/errors.mjs +3 -1
- package/src/main.mjs +5 -24
- package/src/oauth-client.mjs +153 -0
- package/src/oauth-loopback.mjs +120 -0
- package/src/session-files.mjs +213 -0
- package/src/session-store.mjs +177 -64
package/src/api-client.mjs
CHANGED
|
@@ -1,97 +1,138 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
|
|
3
2
|
import { CliError } from "./errors.mjs";
|
|
3
|
+
import { validateBearerToken, validatedUrl } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
const APPLICATIONS = "/v1/applications";
|
|
6
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
7
|
+
const API_ERRORS = new Set(["invalid_request", "invalid_token", "insufficient_scope", "application_not_found",
|
|
8
|
+
"idempotency_conflict", "secret_revoked", "dependency_unavailable", "rate_limited"]);
|
|
4
9
|
|
|
5
10
|
export class ManagementApiClient {
|
|
6
|
-
/** @param {{
|
|
7
|
-
constructor({
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
url = new URL(baseUrl);
|
|
11
|
-
} catch {
|
|
12
|
-
throw new CliError("REQUESTSHIELD_API_URL must be a valid URL", { exitCode: 2 });
|
|
13
|
-
}
|
|
14
|
-
if (url.protocol !== "https:" && !isLoopback(url)) {
|
|
15
|
-
throw new CliError("The management API must use HTTPS except on loopback", {
|
|
16
|
-
exitCode: 2,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
this.baseUrl = url.href.replace(/\/$/, "");
|
|
11
|
+
/** @param {{baseUrl: string, fetchImpl?: typeof fetch, timeoutMs?: number}} options */
|
|
12
|
+
constructor({baseUrl, fetchImpl = fetch, timeoutMs = 15_000}) {
|
|
13
|
+
this.baseUrl = validatedUrl(baseUrl, "API_URL");
|
|
20
14
|
this.fetchImpl = fetchImpl;
|
|
15
|
+
this.timeoutMs = timeoutMs;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
/** @param {string} token @param {{limit?: number, cursor?: string}} [options] */
|
|
19
|
+
listApps(token, options = {}) {
|
|
20
|
+
const query = new URLSearchParams();
|
|
21
|
+
if (options.limit !== undefined) query.set("limit", String(options.limit));
|
|
22
|
+
if (options.cursor !== undefined) query.set("cursor", options.cursor);
|
|
23
|
+
return this.#request(`${APPLICATIONS}${query.size ? `?${query}` : ""}`, token, "GET");
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
/** @param {string}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
26
|
+
/** @param {string} token @param {string} appKey */
|
|
27
|
+
getApp(token, appKey) { return this.#request(appPath(appKey), token, "GET"); }
|
|
28
|
+
|
|
29
|
+
/** @param {string} token @param {{name: string, idempotencyKey: string}} options */
|
|
30
|
+
createApp(token, {name, idempotencyKey}) {
|
|
31
|
+
return this.#request(APPLICATIONS, token, "POST", {name}, idempotencyKey);
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
/** @param {string}
|
|
40
|
-
|
|
41
|
-
return this.#request("
|
|
42
|
-
method: "POST",
|
|
43
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
44
|
-
body: "{}",
|
|
45
|
-
});
|
|
34
|
+
/** @param {string} token @param {string} appKey @param {{name: string, idempotencyKey: string}} options */
|
|
35
|
+
renameApp(token, appKey, {name, idempotencyKey}) {
|
|
36
|
+
return this.#request(appPath(appKey), token, "PATCH", {name}, idempotencyKey);
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
async #request(path, init, allowErrorBody = false) {
|
|
54
|
-
let response;
|
|
55
|
-
try {
|
|
56
|
-
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
57
|
-
...init,
|
|
58
|
-
headers: { "Content-Type": "application/json", ...init.headers },
|
|
59
|
-
signal: AbortSignal.timeout(15_000),
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
throw new CliError(`Could not reach the RequestShield API: ${messageOf(error)}`, {
|
|
63
|
-
code: "NETWORK_ERROR",
|
|
64
|
-
exitCode: 7,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
39
|
+
/** @param {string} token @param {string} appKey @param {{enabled: boolean, idempotencyKey: string}} options */
|
|
40
|
+
setAppEnabled(token, appKey, {enabled, idempotencyKey}) {
|
|
41
|
+
return this.#request(`${appPath(appKey)}/${enabled ? "enable" : "disable"}`, token, "POST", undefined, idempotencyKey);
|
|
42
|
+
}
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
44
|
+
/** @param {string} token @param {string} appKey @param {{idempotencyKey: string}} options */
|
|
45
|
+
rotateSecret(token, appKey, {idempotencyKey}) {
|
|
46
|
+
return this.#request(`${appPath(appKey)}/secret/rotate`, token, "POST", undefined, idempotencyKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} token @param {string} appKey */
|
|
50
|
+
revealSecret(token, appKey) { return this.#request(`${appPath(appKey)}/secret/reveal`, token, "POST"); }
|
|
51
|
+
|
|
52
|
+
/** @param {string} token @param {string} appKey @param {{idempotencyKey: string}} options */
|
|
53
|
+
revokeSecret(token, appKey, {idempotencyKey}) {
|
|
54
|
+
return this.#request(`${appPath(appKey)}/secret/revoke`, token, "POST", undefined, idempotencyKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** No retries: a lost mutation response may represent a committed operation.
|
|
58
|
+
* @param {string} path @param {string} token @param {string} method
|
|
59
|
+
* @param {{name: string}} [payload] @param {string} [idempotencyKey]
|
|
60
|
+
*/
|
|
61
|
+
async #request(path, token, method, payload, idempotencyKey) {
|
|
62
|
+
const headers = {Accept: "application/json", Authorization: `Bearer ${validateBearerToken(token)}`};
|
|
63
|
+
if (idempotencyKey !== undefined) {
|
|
64
|
+
if (!/^[A-Za-z0-9._~-]{1,128}$/.test(idempotencyKey)) {
|
|
65
|
+
throw new CliError("Invalid idempotency key", {code: "INVALID_ARGUMENT", exitCode: 2});
|
|
77
66
|
}
|
|
67
|
+
Object.assign(headers, {"Idempotency-Key": idempotencyKey});
|
|
78
68
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
if (payload !== undefined) Object.assign(headers, {"Content-Type": "application/json"});
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
/** @type {ReadableStreamDefaultReader<Uint8Array> | undefined} */
|
|
72
|
+
let reader;
|
|
73
|
+
let expired = false;
|
|
74
|
+
const deadline = new Promise((_, reject) => {
|
|
75
|
+
controller.signal.addEventListener("abort", () => reject(new CliError(
|
|
76
|
+
"The RequestShield API request timed out. Check connectivity and try again.",
|
|
77
|
+
{code: "REQUEST_TIMEOUT", exitCode: 7},
|
|
78
|
+
)), {once: true});
|
|
79
|
+
});
|
|
80
|
+
const timer = setTimeout(() => { expired = true; controller.abort(); }, this.timeoutMs);
|
|
81
|
+
try {
|
|
82
|
+
const operation = (async () => {
|
|
83
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
84
|
+
method, headers, ...(payload !== undefined ? {body: JSON.stringify(payload)} : {}),
|
|
85
|
+
signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store",
|
|
86
|
+
});
|
|
87
|
+
if (expired) { void response.body?.cancel().catch(() => {}); throw new Error(); }
|
|
88
|
+
if (Number(response.headers.get("content-length")) > MAX_BODY_BYTES) {
|
|
89
|
+
void response.body?.cancel().catch(() => {}); throw invalidResponse();
|
|
90
|
+
}
|
|
91
|
+
const chunks = [];
|
|
92
|
+
let size = 0;
|
|
93
|
+
if (response.body) {
|
|
94
|
+
reader = response.body.getReader();
|
|
95
|
+
while (true) {
|
|
96
|
+
const {done, value} = await reader.read();
|
|
97
|
+
if (expired) throw new Error();
|
|
98
|
+
if (done) break;
|
|
99
|
+
size += value.length;
|
|
100
|
+
if (size > MAX_BODY_BYTES) throw invalidResponse();
|
|
101
|
+
chunks.push(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let body;
|
|
105
|
+
try { body = JSON.parse(new TextDecoder("utf-8", {fatal: true}).decode(Buffer.concat(chunks))); }
|
|
106
|
+
catch { if (response.ok) throw invalidResponse(); }
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const providerCode = objectString(body?.error, "code");
|
|
109
|
+
const safeCode = providerCode && API_ERRORS.has(providerCode) ? providerCode : undefined;
|
|
110
|
+
const guidance = response.status === 401 ? "; run `requestshield signin` again"
|
|
111
|
+
: safeCode === "insufficient_scope" ? "; this session lacks the required permissions" : "";
|
|
112
|
+
throw new CliError(`RequestShield API error: HTTP ${response.status}${safeCode ? ` (${safeCode})` : ""}${guidance}`, {
|
|
113
|
+
code: response.status === 401 ? "UNAUTHENTICATED" : "API_ERROR",
|
|
114
|
+
exitCode: response.status === 401 || response.status === 403 ? 3 : 1,
|
|
115
|
+
httpStatus: response.status, apiErrorCode: safeCode,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw invalidResponse();
|
|
119
|
+
return {ok: true, status: response.status, body, replayed: response.headers.get("Idempotency-Replayed") === "true"};
|
|
120
|
+
})();
|
|
121
|
+
return await Promise.race([operation, /** @type {Promise<never>} */ (deadline)]);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof CliError) throw error;
|
|
124
|
+
throw new CliError("Could not reach the RequestShield API", {code: "NETWORK_ERROR", exitCode: 7});
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
if (reader) { void reader.cancel().catch(() => {}); }
|
|
128
|
+
controller.abort();
|
|
86
129
|
}
|
|
87
|
-
return { ok: response.ok, status: response.status, body };
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
132
|
|
|
91
|
-
/** @param {
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
}
|
|
133
|
+
/** @param {string} appKey */
|
|
134
|
+
function appPath(appKey) { return `${APPLICATIONS}/${encodeURIComponent(appKey)}`; }
|
|
135
|
+
function invalidResponse() { return new CliError("The RequestShield API returned an invalid or oversized response", {code: "INVALID_RESPONSE"}); }
|
|
95
136
|
|
|
96
137
|
/** @param {unknown} value @param {string} property */
|
|
97
138
|
export function objectString(value, property) {
|
|
@@ -99,8 +140,3 @@ export function objectString(value, property) {
|
|
|
99
140
|
const found = Reflect.get(value, property);
|
|
100
141
|
return typeof found === "string" ? found : undefined;
|
|
101
142
|
}
|
|
102
|
-
|
|
103
|
-
/** @param {unknown} error */
|
|
104
|
-
function messageOf(error) {
|
|
105
|
-
return error instanceof Error ? error.message : String(error);
|
|
106
|
-
}
|
package/src/args.mjs
CHANGED
|
@@ -1,85 +1,145 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
requestshield
|
|
9
|
-
requestshield
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { CliError } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `RequestShield CLI
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
requestshield signin [--no-open]
|
|
8
|
+
requestshield auth status [--json]
|
|
9
|
+
requestshield signout
|
|
10
|
+
requestshield keys create --app-name <name> [--idempotency-key <key>]
|
|
11
|
+
requestshield keys rotate <app-key> [--idempotency-key <key>] [--yes]
|
|
12
|
+
requestshield keys reveal <app-key> [--yes]
|
|
13
|
+
requestshield keys revoke <app-key> [--idempotency-key <key>] [--yes]
|
|
14
|
+
requestshield apps list [--json] [--limit <1-100>] [--cursor <cursor> | --all]
|
|
15
|
+
requestshield apps get <app-key>
|
|
16
|
+
requestshield apps rename <app-key> --name <name> [--idempotency-key <key>]
|
|
17
|
+
requestshield apps enable <app-key> [--idempotency-key <key>]
|
|
18
|
+
requestshield apps disable <app-key> [--idempotency-key <key>] [--yes]
|
|
10
19
|
requestshield agent setup [--force]
|
|
11
20
|
requestshield agent setup --codex [--force]
|
|
12
21
|
requestshield agent setup --claude [--force]
|
|
13
22
|
requestshield update check
|
|
14
23
|
requestshield --help
|
|
15
|
-
requestshield --version
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* | {
|
|
22
|
-
* | { command:
|
|
23
|
-
* | {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (argv
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
24
|
+
requestshield --version
|
|
25
|
+
|
|
26
|
+
List defaults to one page. Use --all for a bounded complete listing.
|
|
27
|
+
contract, challenge volume and get billing are not available yet.`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{command:"help",help:string} | {command:"version"|"update-check"|"signout"}
|
|
31
|
+
* | {command:"signin",noOpen:boolean} | {command:"auth-status",json:boolean}
|
|
32
|
+
* | {command:"keys-create",appName:string,yes:boolean,idempotencyKey?:string}
|
|
33
|
+
* | {command:"keys-rotate"|"keys-reveal"|"keys-revoke",appKey:string,yes:boolean,idempotencyKey?:string}
|
|
34
|
+
* | {command:"apps-list",json:boolean,all:boolean,limit?:number,cursor?:string}
|
|
35
|
+
* | {command:"apps-get",appKey:string}
|
|
36
|
+
* | {command:"apps-rename",appKey:string,name:string,yes:boolean,idempotencyKey?:string}
|
|
37
|
+
* | {command:"apps-enable"|"apps-disable",appKey:string,enabled:boolean,yes:boolean,idempotencyKey?:string}
|
|
38
|
+
* | {command:"agent-setup",agent?:"codex"|"claude",force:boolean}} ParsedArgs
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** @param {string[]} argv @returns {ParsedArgs} */
|
|
42
|
+
export function parseArgs(argv) {
|
|
43
|
+
if (!argv.length || argv.includes("--help") || argv.includes("-h")) return {command:"help",help:HELP};
|
|
44
|
+
if (argv.length === 1 && ["--version","-v","-V"].includes(argv[0])) return {command:"version"};
|
|
45
|
+
const [group, action] = argv;
|
|
46
|
+
if (group === "contract" || group === "challenge" && action === "volume" || group === "get" && action === "billing") {
|
|
47
|
+
throw new CliError("This command is not available: its Management API endpoint has not been implemented.", {code:"COMMAND_UNAVAILABLE",exitCode:2});
|
|
48
|
+
}
|
|
49
|
+
if (group === "signin") {
|
|
50
|
+
const flags = parseFlags(argv.slice(1), ["--no-open"]);
|
|
51
|
+
return {command:"signin",noOpen:flags.has("--no-open")};
|
|
52
|
+
}
|
|
53
|
+
if (group === "signout") { parseFlags(argv.slice(1), []); return {command:"signout"}; }
|
|
54
|
+
if (group === "auth" && action === "status") {
|
|
55
|
+
const flags = parseFlags(argv.slice(2), ["--json"]);
|
|
56
|
+
return {command:"auth-status",json:flags.has("--json")};
|
|
57
|
+
}
|
|
58
|
+
if (group === "update" && action === "check") { parseFlags(argv.slice(2), []); return {command:"update-check"}; }
|
|
59
|
+
if (group === "keys" && action === "create") {
|
|
60
|
+
const flags = parseFlags(argv.slice(2), ["--yes"], ["--app-name","--idempotency-key"]);
|
|
61
|
+
const name = flags.get("--app-name");
|
|
62
|
+
if (typeof name !== "string") usage("keys create --app-name <name> [--idempotency-key <key>]");
|
|
63
|
+
return {command:"keys-create",appName:applicationName(name),...mutationFlags(flags)};
|
|
64
|
+
}
|
|
65
|
+
if (group === "keys" && ["rotate","reveal","revoke"].includes(action)) {
|
|
66
|
+
const appKey = requiredAppKey(argv, `keys ${action} <app-key>`);
|
|
67
|
+
const flags = parseFlags(argv.slice(3), ["--yes"], action === "reveal" ? [] : ["--idempotency-key"]);
|
|
68
|
+
return {command:/** @type {"keys-rotate"|"keys-reveal"|"keys-revoke"} */ (`keys-${action}`),appKey,...mutationFlags(flags)};
|
|
69
|
+
}
|
|
70
|
+
if (group === "apps" && action === "list") {
|
|
71
|
+
const flags = parseFlags(argv.slice(2), ["--json","--all"], ["--limit","--cursor"]);
|
|
72
|
+
const limit = flags.get("--limit");
|
|
73
|
+
if (limit !== undefined && (typeof limit !== "string" || !/^[1-9][0-9]{0,2}$/.test(limit) || Number(limit) > 100)) {
|
|
74
|
+
throw invalid("--limit must be an integer from 1 to 100");
|
|
75
|
+
}
|
|
76
|
+
const cursor = flags.get("--cursor");
|
|
77
|
+
if (cursor !== undefined && (typeof cursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor))) throw invalid("Invalid --cursor");
|
|
78
|
+
if (cursor !== undefined && flags.has("--all")) throw invalid("--cursor and --all cannot be combined");
|
|
79
|
+
return {command:"apps-list",json:flags.has("--json"),all:flags.has("--all"),
|
|
80
|
+
...(limit !== undefined ? {limit:Number(limit)} : {}),...(typeof cursor === "string" ? {cursor} : {})};
|
|
81
|
+
}
|
|
82
|
+
if (group === "apps" && action === "get") {
|
|
83
|
+
const appKey = requiredAppKey(argv, "apps get <app-key>");
|
|
84
|
+
parseFlags(argv.slice(3), []);
|
|
85
|
+
return {command:"apps-get",appKey};
|
|
86
|
+
}
|
|
87
|
+
if (group === "apps" && ["rename","enable","disable"].includes(action)) {
|
|
88
|
+
const appKey = requiredAppKey(argv, `apps ${action} <app-key>${action === "rename" ? " --name <name>" : ""}`);
|
|
89
|
+
const flags = parseFlags(argv.slice(3), ["--yes"], ["--idempotency-key",...(action === "rename" ? ["--name"] : [])]);
|
|
90
|
+
if (action === "rename") {
|
|
91
|
+
const name = flags.get("--name");
|
|
92
|
+
if (typeof name !== "string") usage("apps rename <app-key> --name <name>");
|
|
93
|
+
return {command:"apps-rename",appKey,name:applicationName(name),...mutationFlags(flags)};
|
|
94
|
+
}
|
|
95
|
+
return {command:action === "enable" ? "apps-enable" : "apps-disable",appKey,enabled:action === "enable",...mutationFlags(flags)};
|
|
96
|
+
}
|
|
97
|
+
if (group === "agent" && action === "setup") {
|
|
98
|
+
const flags = parseFlags(argv.slice(2), ["--codex","--claude","--force"]);
|
|
99
|
+
if (flags.has("--codex") && flags.has("--claude")) throw invalid("Only one of --codex or --claude may be provided");
|
|
100
|
+
return {command:"agent-setup",force:flags.has("--force"),
|
|
101
|
+
...(flags.has("--codex") ? {agent:/** @type {const} */ ("codex")} : flags.has("--claude") ? {agent:/** @type {const} */ ("claude")} : {})};
|
|
102
|
+
}
|
|
103
|
+
throw invalid(`Unknown command.\n\n${HELP}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string[]} args @param {string[]} booleans @param {string[]} [values] */
|
|
107
|
+
function parseFlags(args, booleans, values = []) {
|
|
108
|
+
/** @type {Map<string,string|true>} */
|
|
109
|
+
const flags = new Map();
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const arg = args[i];
|
|
112
|
+
if (!booleans.includes(arg) && !values.includes(arg)) throw invalid(`Unknown option: ${arg}`);
|
|
113
|
+
if (flags.has(arg)) throw invalid(`${arg} may only be provided once`);
|
|
114
|
+
if (booleans.includes(arg)) flags.set(arg, true);
|
|
115
|
+
else {
|
|
116
|
+
const value = args[++i];
|
|
117
|
+
if (!value || value.startsWith("--")) throw invalid(`${arg} requires a value`);
|
|
118
|
+
flags.set(arg,value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return flags;
|
|
122
|
+
}
|
|
123
|
+
/** @param {Map<string,string|true>} flags */
|
|
124
|
+
function mutationFlags(flags) {
|
|
125
|
+
const key = flags.get("--idempotency-key");
|
|
126
|
+
if (key !== undefined && (typeof key !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/.test(key))) throw invalid("Invalid --idempotency-key");
|
|
127
|
+
return {yes:flags.has("--yes"),...(typeof key === "string" ? {idempotencyKey:key} : {})};
|
|
128
|
+
}
|
|
129
|
+
/** @param {string[]} argv @param {string} help */
|
|
130
|
+
function requiredAppKey(argv, help) {
|
|
131
|
+
if (!argv[2] || argv[2].startsWith("--")) usage(help);
|
|
132
|
+
if (!/^[A-Za-z0-9._~-]{1,128}$/.test(argv[2])) throw invalid("App Key must be 1-128 letters, numbers, '.', '_', '~' or '-'");
|
|
133
|
+
return argv[2];
|
|
134
|
+
}
|
|
135
|
+
/** @param {string} value */
|
|
136
|
+
function applicationName(value) {
|
|
137
|
+
const name = value.trim();
|
|
138
|
+
if (!name || [...name].length > 100 || /[\p{Cc}\p{Cs}]/u.test(name)) throw invalid("Application name must contain 1-100 characters without control characters");
|
|
139
|
+
return name;
|
|
140
|
+
}
|
|
141
|
+
/** @param {string} command @returns {never} */
|
|
142
|
+
function usage(command) { throw invalid(`Usage: requestshield ${command}`); }
|
|
143
|
+
/** @param {string} message */
|
|
144
|
+
function invalid(message) { return new CliError(message,{code:"INVALID_ARGUMENT",exitCode:2}); }
|
|
145
|
+
export { HELP };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { CliError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
/** Uses argument arrays and a fixed Windows script; never interpolate URL into shell code.
|
|
6
|
+
* @param {string} url
|
|
7
|
+
* @param {{signal?: AbortSignal, platform?: NodeJS.Platform, spawnImpl?: typeof spawn}} [options]
|
|
8
|
+
*/
|
|
9
|
+
export async function openBrowser(url, {signal, platform = process.platform, spawnImpl = spawn} = {}) {
|
|
10
|
+
const failure = () => new CliError("Could not open the browser automatically", {code: "BROWSER_OPEN_FAILED"});
|
|
11
|
+
const command = platform === "win32" ? "powershell.exe" : platform === "darwin" ? "open" : "xdg-open";
|
|
12
|
+
const args = platform === "win32"
|
|
13
|
+
? ["-NoProfile", "-NonInteractive", "-Command", "Start-Process -FilePath $env:REQUESTSHIELD_SIGNIN_BROWSER_URL"]
|
|
14
|
+
: [url];
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
if (signal?.aborted) { reject(failure()); return; }
|
|
17
|
+
let child;
|
|
18
|
+
try {
|
|
19
|
+
child = spawnImpl(command, args, {
|
|
20
|
+
windowsHide: true, stdio: "ignore", shell: false,
|
|
21
|
+
env: platform === "win32" ? {...process.env, REQUESTSHIELD_SIGNIN_BROWSER_URL: url} : process.env,
|
|
22
|
+
});
|
|
23
|
+
} catch { reject(failure()); return; }
|
|
24
|
+
const abort = () => { cleanup(); child.kill(); reject(failure()); };
|
|
25
|
+
const timer = setTimeout(abort, 10_000);
|
|
26
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
|
|
27
|
+
child.once("error", () => { cleanup(); reject(failure()); });
|
|
28
|
+
child.once("exit", code => { cleanup(); code === 0 ? resolve(undefined) : reject(failure()); });
|
|
29
|
+
signal?.addEventListener("abort", abort, {once: true});
|
|
30
|
+
if (signal?.aborted) abort();
|
|
31
|
+
});
|
|
32
|
+
}
|