requestshield 0.1.4 → 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/src/args.mjs CHANGED
@@ -1,85 +1,188 @@
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
+
3
+ import { CliError } from "./errors.mjs";
4
+
5
+ const HELP = `RequestShield CLI
6
+
7
+ Usage:
8
+ requestshield signin
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]
13
18
  requestshield update check
14
19
  requestshield --help
15
20
  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 };
21
+
22
+ /**
23
+ * @typedef {{ command: "help", help: string }
24
+ * | { command: "version" }
25
+ * | { command: "update-check" }
26
+ * | { command: "signin" }
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 }
33
+ * | { command: "agent-setup", agent?: "codex" | "claude", force: boolean }} ParsedArgs
34
+ */
35
+
36
+ /** @param {string[]} argv @returns {ParsedArgs} */
37
+ export function parseArgs(argv) {
38
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
39
+ return { command: "help", help: HELP };
40
+ }
41
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V")) {
42
+ return { command: "version" };
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" };
51
+ }
52
+ if (argv[0] === "keys" && argv[1] === "create") {
53
+ assertOnly(argv.slice(2), new Set(["--yes"]));
54
+ return { command: "keys-create", yes: argv.includes("--yes") };
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
+ }
79
+ if (argv[0] === "agent" && argv[1] === "setup") {
80
+ const rest = argv.slice(2);
81
+ /** @type {"codex" | "claude" | undefined} */
82
+ let agent;
83
+ let force = false;
84
+ for (let index = 0; index < rest.length; index++) {
85
+ const arg = rest[index];
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
+ }
99
+ }
100
+ return {
101
+ command: "agent-setup",
102
+ ...(agent ? { agent } : {}),
103
+ force,
104
+ };
105
+ }
106
+ throw new CliError(`Unknown command.\n\n${HELP}`, { exitCode: 2 });
107
+ }
108
+
109
+ /** @param {string[]} args @param {Set<string>} allowed */
110
+ function assertOnly(args, allowed) {
111
+ for (const arg of args) {
112
+ if (!allowed.has(arg)) {
113
+ throw new CliError(`Unknown option: ${arg}`, { exitCode: 2 });
114
+ }
115
+ }
116
+ }
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
+
188
+ export { HELP };
package/src/cli.mjs CHANGED
@@ -1,51 +1,255 @@
1
- #!/usr/bin/env node
2
- // @ts-check
3
-
4
- import { parseArgs } from "./args.mjs";
5
- import { ManagementApiClient } from "./api-client.mjs";
6
- import { SessionStore } from "./session-store.mjs";
7
- import { signin } from "./commands/signin.mjs";
8
- import { createKeys } from "./commands/keys-create.mjs";
9
- import { setupAgent } from "./commands/agent-setup.mjs";
10
- import { checkForUpdate } from "./commands/update-check.mjs";
11
- import packageJson from "../package.json" with { type: "json" };
12
-
13
- /**
14
- * @param {string[]} argv
15
- * @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude">, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean>, fetchImpl?: typeof fetch, confirmUpdate?: () => Promise<boolean>, installLatest?: (packageName: string, version: string) => Promise<void> }} [deps]
16
- */
17
- export async function run(argv, deps = {}) {
18
- const parsed = parseArgs(argv);
19
- const log = deps.log ?? ((message) => console.log(message));
20
- if (parsed.command === "help") return log(parsed.help);
21
- if (parsed.command === "version") return log("requestshield " + packageJson.version);
22
- if (parsed.command === "update-check") {
23
- return checkForUpdate({
24
- currentVersion: packageJson.version,
25
- packageName: packageJson.name,
26
- log,
27
- fetchImpl: deps.fetchImpl,
28
- confirm: deps.confirmUpdate,
29
- install: deps.installLatest,
30
- });
31
- }
32
-
33
- if (parsed.command === "agent-setup") {
34
- return setupAgent(parsed, { log, ...deps });
35
- }
36
-
37
- const env = deps.env ?? process.env;
38
-
39
- const api = deps.api ?? new ManagementApiClient({
40
- baseUrl:
41
- env.REQUESTSHIELD_API_URL ??
42
- "https://api.intellifend.ai",
43
- });
44
-
45
- const sessions = deps.sessions ?? new SessionStore({
46
- env,
47
- homeDir: deps.homeDir,
48
- });
49
- if (parsed.command === "signin") return signin({ api, sessions, log, ...deps });
50
- if (parsed.command === "keys-create") return createKeys(parsed, { api, sessions, log, ...deps });
51
- }
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import { parseArgs } from "./args.mjs";
5
+ import { ManagementApiClient } from "./api-client.mjs";
6
+ import { SessionStore } from "./session-store.mjs";
7
+
8
+ import { signin } from "./commands/signin.mjs";
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";
15
+ import { setupAgent } from "./commands/agent-setup.mjs";
16
+ import { checkForUpdate } from "./commands/update-check.mjs";
17
+
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
+ }
159
+
160
+ /**
161
+ * @param {string[]} argv
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]
183
+ */
184
+ export async function run(argv, deps = {}) {
185
+ const parsed = parseArgs(argv);
186
+
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
+ );
200
+ }
201
+
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;
219
+
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
+ }
247
+
248
+ /*
249
+ * Execute commands that do not need an API session.
250
+ */
251
+ return command.handler(
252
+ parsed,
253
+ context
254
+ );
255
+ }