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.
Files changed (37) hide show
  1. package/README.md +421 -85
  2. package/config/.env.prod +7 -0
  3. package/package.json +21 -12
  4. package/skills/requestshield/SKILL.md +299 -307
  5. package/skills/requestshield/assets/AGENTS.codex.md +62 -62
  6. package/skills/requestshield/references/backend-java-core.md +128 -128
  7. package/skills/requestshield/references/backend-spring-boot.md +145 -145
  8. package/skills/requestshield/references/browser-manual.md +210 -210
  9. package/skills/requestshield/references/browser-seamless.md +156 -164
  10. package/skills/requestshield/references/cli.md +107 -182
  11. package/skills/requestshield/references/integration-planning.md +362 -389
  12. package/skills/requestshield/references/troubleshooting.md +114 -118
  13. package/src/agent-detector.mjs +102 -74
  14. package/src/api-client.mjs +115 -79
  15. package/src/args.mjs +140 -80
  16. package/src/browser-opener.mjs +32 -0
  17. package/src/cli.mjs +277 -51
  18. package/src/commands/agent-setup.mjs +182 -185
  19. package/src/commands/application-mutations.mjs +33 -0
  20. package/src/commands/application-response.mjs +55 -0
  21. package/src/commands/apps-get.mjs +20 -0
  22. package/src/commands/apps-list.mjs +94 -0
  23. package/src/commands/auth-status.mjs +37 -0
  24. package/src/commands/keys-create.mjs +7 -38
  25. package/src/commands/mutation-support.mjs +110 -0
  26. package/src/commands/secret-commands.mjs +45 -0
  27. package/src/commands/signin.mjs +70 -57
  28. package/src/commands/signout.mjs +9 -0
  29. package/src/commands/update-check.mjs +12 -4
  30. package/src/config.mjs +150 -0
  31. package/src/entrypoint.mjs +24 -0
  32. package/src/errors.mjs +3 -1
  33. package/src/main.mjs +5 -24
  34. package/src/oauth-client.mjs +153 -0
  35. package/src/oauth-loopback.mjs +120 -0
  36. package/src/session-files.mjs +213 -0
  37. package/src/session-store.mjs +177 -64
@@ -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 {{ baseUrl: string, fetchImpl?: typeof fetch }} options */
7
- constructor({ baseUrl, fetchImpl = fetch }) {
8
- let url;
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
- async startSignin() {
24
- return this.#request("/v1/cli/signin", {
25
- method: "POST",
26
- body: JSON.stringify({ client: "requestshield-cli" }),
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} deviceCode */
31
- async pollSignin(deviceCode) {
32
- return this.#request(
33
- "/v1/cli/signin/token",
34
- { method: "POST", body: JSON.stringify({ deviceCode }) },
35
- true,
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} accessToken */
40
- async rotateKeys(accessToken) {
41
- return this.#request("/v1/cli/keys/rotate", {
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
- * @param {string} path
50
- * @param {RequestInit} init
51
- * @param {boolean} [allowErrorBody]
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
- const text = await response.text();
69
- let body = {};
70
- if (text !== "") {
71
- try {
72
- body = JSON.parse(text);
73
- } catch {
74
- throw new CliError("The RequestShield API returned invalid JSON", {
75
- code: "INVALID_RESPONSE",
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 (!response.ok && !allowErrorBody) {
80
- const detail = objectString(body, "message") ?? `HTTP ${response.status}`;
81
- const exitCode = response.status === 401 || response.status === 403 ? 3 : 1;
82
- throw new CliError(`RequestShield API error: ${detail}`, {
83
- code: objectString(body, "code") ?? "API_ERROR",
84
- exitCode,
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 {URL} url */
92
- function isLoopback(url) {
93
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
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
- import { CliError } from "./errors.mjs";
4
-
5
- const HELP = `RequestShield CLI
6
-
7
- Usage:
8
- requestshield signin
9
- requestshield keys create [--yes]
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
- * @typedef {{ command: "help", help: string }
19
- * | { command: "version" }
20
- * | { command: "update-check" }
21
- * | { command: "signin" }
22
- * | { command: "keys-create", yes: boolean }
23
- * | { command: "agent-setup", agent?: "codex" | "claude", force: boolean }} ParsedArgs
24
- */
25
-
26
- /** @param {string[]} argv @returns {ParsedArgs} */
27
- export function parseArgs(argv) {
28
- if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
29
- return { command: "help", help: HELP };
30
- }
31
- if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V")) {
32
- return { command: "version" };
33
- }
34
- if (argv[0] === "update" && argv[1] === "check") {
35
- assertOnly(argv.slice(2), new Set());
36
- return { command: "update-check" };
37
- }
38
- if (argv[0] === "signin") {
39
- assertOnly(argv.slice(1), new Set());
40
- return { command: "signin" };
41
- }
42
- if (argv[0] === "keys" && argv[1] === "create") {
43
- assertOnly(argv.slice(2), new Set(["--yes"]));
44
- return { command: "keys-create", yes: argv.includes("--yes") };
45
- }
46
- if (argv[0] === "agent" && argv[1] === "setup") {
47
- const rest = argv.slice(2);
48
- /** @type {"codex" | "claude" | undefined} */
49
- let agent;
50
- let force = false;
51
- for (let index = 0; index < rest.length; index++) {
52
- const arg = rest[index];
53
- if (arg === "--force") {
54
- force = true;
55
- } else if (arg === "--codex" || arg === "--claude") {
56
- if (agent !== undefined) {
57
- throw new CliError("Only one of --codex or --claude may be provided", {
58
- code: "INVALID_AGENT",
59
- exitCode: 2,
60
- });
61
- }
62
- agent = arg === "--codex" ? "codex" : "claude";
63
- } else {
64
- throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
65
- }
66
- }
67
- return {
68
- command: "agent-setup",
69
- ...(agent ? { agent } : {}),
70
- force,
71
- };
72
- }
73
- throw new CliError(`Unknown command.\n\n${HELP}`, { exitCode: 2 });
74
- }
75
-
76
- /** @param {string[]} args @param {Set<string>} allowed */
77
- function assertOnly(args, allowed) {
78
- for (const arg of args) {
79
- if (!allowed.has(arg)) {
80
- throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
81
- }
82
- }
83
- }
84
-
85
- export { HELP };
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
+ }