requestshield 0.1.5 → 0.1.7
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 +407 -269
- package/config/.env.prod +5 -0
- package/package.json +8 -5
- package/skills/requestshield/SKILL.md +55 -63
- package/skills/requestshield/assets/AGENTS.codex.md +17 -17
- package/skills/requestshield/references/backend-java-core.md +3 -3
- package/skills/requestshield/references/backend-spring-boot.md +3 -3
- package/skills/requestshield/references/browser-manual.md +4 -4
- package/skills/requestshield/references/browser-seamless.md +7 -15
- package/skills/requestshield/references/cli.md +93 -169
- package/skills/requestshield/references/integration-planning.md +19 -46
- package/skills/requestshield/references/troubleshooting.md +26 -30
- package/src/api-client.mjs +106 -165
- package/src/args.mjs +108 -151
- package/src/browser-opener.mjs +32 -0
- package/src/cli.mjs +50 -28
- package/src/commands/agent-setup.mjs +34 -37
- package/src/commands/application-mutations.mjs +33 -0
- package/src/commands/application-response.mjs +55 -0
- package/src/commands/apps-get.mjs +3 -47
- package/src/commands/apps-list.mjs +40 -36
- 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 +145 -3
- package/src/entrypoint.mjs +24 -0
- package/src/errors.mjs +3 -1
- package/src/main.mjs +2 -21
- 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/commands/billing-get.mjs +0 -110
- package/src/commands/challenge-volume.mjs +0 -81
- package/src/commands/contract.mjs +0 -106
package/src/api-client.mjs
CHANGED
|
@@ -1,192 +1,138 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
|
|
3
2
|
import { CliError } from "./errors.mjs";
|
|
3
|
+
import { validateBearerToken, validatedUrl } from "./config.mjs";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
INTEGRATION_CONTRACT: "/v1/integration-contract",
|
|
10
|
-
APPLICATIONS: "/v1/applications",
|
|
11
|
-
};
|
|
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"]);
|
|
12
9
|
|
|
13
10
|
export class ManagementApiClient {
|
|
14
|
-
/** @param {{
|
|
15
|
-
constructor({
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
url = new URL(baseUrl);
|
|
19
|
-
} catch {
|
|
20
|
-
throw new CliError("REQUESTSHIELD_API_URL must be a valid URL", { exitCode: 2 });
|
|
21
|
-
}
|
|
22
|
-
if (url.protocol !== "https:" && !isLoopback(url)) {
|
|
23
|
-
throw new CliError("The management API must use HTTPS except on loopback", {
|
|
24
|
-
exitCode: 2,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
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");
|
|
28
14
|
this.fetchImpl = fetchImpl;
|
|
15
|
+
this.timeoutMs = timeoutMs;
|
|
29
16
|
}
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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");
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
/** @param {string}
|
|
39
|
-
|
|
40
|
-
return this.#request(
|
|
41
|
-
API_ENDPOINTS.SIGNIN_TOKEN,
|
|
42
|
-
{ method: "POST", body: JSON.stringify({ deviceCode }) },
|
|
43
|
-
true,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
26
|
+
/** @param {string} token @param {string} appKey */
|
|
27
|
+
getApp(token, appKey) { return this.#request(appPath(appKey), token, "GET"); }
|
|
46
28
|
|
|
47
|
-
/** @param {string}
|
|
48
|
-
|
|
49
|
-
return this.#request(
|
|
50
|
-
method: "POST",
|
|
51
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
52
|
-
body: "{}",
|
|
53
|
-
});
|
|
29
|
+
/** @param {string} token @param {{name: string, idempotencyKey: string}} options */
|
|
30
|
+
createApp(token, {name, idempotencyKey}) {
|
|
31
|
+
return this.#request(APPLICATIONS, token, "POST", {name}, idempotencyKey);
|
|
54
32
|
}
|
|
55
33
|
|
|
56
|
-
/** @param {string}
|
|
57
|
-
|
|
58
|
-
return this.#request(
|
|
59
|
-
method: "GET",
|
|
60
|
-
headers: {
|
|
61
|
-
Accept: "application/json",
|
|
62
|
-
Authorization: `Bearer ${accessToken}`,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
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);
|
|
65
37
|
}
|
|
66
38
|
|
|
67
|
-
/** @param {string}
|
|
68
|
-
|
|
69
|
-
return this.#request(
|
|
70
|
-
method: "GET",
|
|
71
|
-
headers: {
|
|
72
|
-
Accept: "application/json",
|
|
73
|
-
Authorization: `Bearer ${accessToken}`,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
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);
|
|
76
42
|
}
|
|
77
43
|
|
|
78
|
-
/** @param {string}
|
|
79
|
-
|
|
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
|
-
);
|
|
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);
|
|
90
47
|
}
|
|
91
48
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
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;
|
|
49
|
+
/** @param {string} token @param {string} appKey */
|
|
50
|
+
revealSecret(token, appKey) { return this.#request(`${appPath(appKey)}/secret/reveal`, token, "POST"); }
|
|
115
51
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
);
|
|
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);
|
|
138
55
|
}
|
|
139
56
|
|
|
140
|
-
/**
|
|
141
|
-
* @param {string} path
|
|
142
|
-
* @param {
|
|
143
|
-
* @param {boolean} [allowErrorBody]
|
|
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]
|
|
144
60
|
*/
|
|
145
|
-
async #request(path,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
headers: { "Content-Type": "application/json", ...init.headers },
|
|
151
|
-
signal: AbortSignal.timeout(15_000),
|
|
152
|
-
});
|
|
153
|
-
} catch (error) {
|
|
154
|
-
throw new CliError(`Could not reach the RequestShield API: ${messageOf(error)}`, {
|
|
155
|
-
code: "NETWORK_ERROR",
|
|
156
|
-
exitCode: 7,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const text = await response.text();
|
|
161
|
-
let body = {};
|
|
162
|
-
if (text !== "") {
|
|
163
|
-
try {
|
|
164
|
-
body = JSON.parse(text);
|
|
165
|
-
} catch {
|
|
166
|
-
throw new CliError("The RequestShield API returned invalid JSON", {
|
|
167
|
-
code: "INVALID_RESPONSE",
|
|
168
|
-
});
|
|
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});
|
|
169
66
|
}
|
|
67
|
+
Object.assign(headers, {"Idempotency-Key": idempotencyKey});
|
|
170
68
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
exitCode,
|
|
180
|
-
});
|
|
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();
|
|
181
129
|
}
|
|
182
|
-
return { ok: response.ok, status: response.status, body };
|
|
183
130
|
}
|
|
184
131
|
}
|
|
185
132
|
|
|
186
|
-
/** @param {
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
}
|
|
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"}); }
|
|
190
136
|
|
|
191
137
|
/** @param {unknown} value @param {string} property */
|
|
192
138
|
export function objectString(value, property) {
|
|
@@ -194,8 +140,3 @@ export function objectString(value, property) {
|
|
|
194
140
|
const found = Reflect.get(value, property);
|
|
195
141
|
return typeof found === "string" ? found : undefined;
|
|
196
142
|
}
|
|
197
|
-
|
|
198
|
-
/** @param {unknown} error */
|
|
199
|
-
function messageOf(error) {
|
|
200
|
-
return error instanceof Error ? error.message : String(error);
|
|
201
|
-
}
|
package/src/args.mjs
CHANGED
|
@@ -1,188 +1,145 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
|
|
3
2
|
import { CliError } from "./errors.mjs";
|
|
4
3
|
|
|
5
4
|
const HELP = `RequestShield CLI
|
|
6
5
|
|
|
7
6
|
Usage:
|
|
8
|
-
requestshield signin
|
|
9
|
-
requestshield
|
|
10
|
-
requestshield
|
|
11
|
-
requestshield
|
|
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]
|
|
12
15
|
requestshield apps get <app-key>
|
|
13
|
-
requestshield
|
|
14
|
-
requestshield
|
|
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]
|
|
15
19
|
requestshield agent setup [--force]
|
|
16
20
|
requestshield agent setup --codex [--force]
|
|
17
21
|
requestshield agent setup --claude [--force]
|
|
18
22
|
requestshield update check
|
|
19
23
|
requestshield --help
|
|
20
|
-
requestshield --version
|
|
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.`;
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
|
-
* @typedef {{
|
|
24
|
-
* | { command:
|
|
25
|
-
* | {
|
|
26
|
-
* | {
|
|
27
|
-
* | {
|
|
28
|
-
* | {
|
|
29
|
-
* | {
|
|
30
|
-
* | {
|
|
31
|
-
* | {
|
|
32
|
-
* | { command: "billing-get", appKey: string }
|
|
33
|
-
* | { command: "agent-setup", agent?: "codex" | "claude", force: boolean }} ParsedArgs
|
|
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
|
|
34
39
|
*/
|
|
35
40
|
|
|
36
41
|
/** @param {string[]} argv @returns {ParsedArgs} */
|
|
37
42
|
export function parseArgs(argv) {
|
|
38
|
-
if (argv.length
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
if (argv[0] === "update" && argv[1] === "check") {
|
|
45
|
-
assertOnly(argv.slice(2), new Set());
|
|
46
|
-
return { command: "update-check" };
|
|
47
|
-
}
|
|
48
|
-
if (argv[0] === "signin") {
|
|
49
|
-
assertOnly(argv.slice(1), new Set());
|
|
50
|
-
return { command: "signin" };
|
|
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});
|
|
51
48
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
return {
|
|
49
|
+
if (group === "signin") {
|
|
50
|
+
const flags = parseFlags(argv.slice(1), ["--no-open"]);
|
|
51
|
+
return {command:"signin",noOpen:flags.has("--no-open")};
|
|
55
52
|
}
|
|
56
|
-
if (argv[
|
|
57
|
-
|
|
58
|
-
|
|
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")};
|
|
59
57
|
}
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
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)};
|
|
63
64
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
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
69
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
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} : {})};
|
|
72
81
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
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};
|
|
78
86
|
}
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (arg === "--force") {
|
|
87
|
-
force = true;
|
|
88
|
-
} else if (arg === "--codex" || arg === "--claude") {
|
|
89
|
-
if (agent !== undefined) {
|
|
90
|
-
throw new CliError("Only one of --codex or --claude may be provided", {
|
|
91
|
-
code: "INVALID_AGENT",
|
|
92
|
-
exitCode: 2,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
agent = arg === "--codex" ? "codex" : "claude";
|
|
96
|
-
} else {
|
|
97
|
-
throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
|
|
98
|
-
}
|
|
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)};
|
|
99
94
|
}
|
|
100
|
-
return {
|
|
101
|
-
command: "agent-setup",
|
|
102
|
-
...(agent ? { agent } : {}),
|
|
103
|
-
force,
|
|
104
|
-
};
|
|
95
|
+
return {command:action === "enable" ? "apps-enable" : "apps-disable",appKey,enabled:action === "enable",...mutationFlags(flags)};
|
|
105
96
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for (const arg of args) {
|
|
112
|
-
if (!allowed.has(arg)) {
|
|
113
|
-
throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
|
|
114
|
-
}
|
|
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")} : {})};
|
|
115
102
|
}
|
|
103
|
+
throw invalid(`Unknown command.\n\n${HELP}`);
|
|
116
104
|
}
|
|
117
105
|
|
|
118
|
-
/** @param {string[]}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
});
|
|
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);
|
|
153
119
|
}
|
|
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
120
|
}
|
|
168
|
-
return
|
|
121
|
+
return flags;
|
|
169
122
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (!/^[A-Za-z0-9._~-]{1,128}$/.test(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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];
|
|
180
134
|
}
|
|
181
|
-
|
|
182
135
|
/** @param {string} value */
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
186
140
|
}
|
|
187
|
-
|
|
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}); }
|
|
188
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
|
+
}
|