requestshield 0.1.0 → 0.1.2

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 CHANGED
@@ -1,96 +1,233 @@
1
1
  # RequestShield CLI
2
2
 
3
- Customer-facing CLI for signing in, rotating RequestShield application keys,
4
- and installing the RequestShield skill for Codex.
3
+ Customer-facing command-line tool for RequestShield. The CLI lets a customer
4
+ sign in, rotate application credentials, install the RequestShield Skill for
5
+ Codex, and check for a newer npm release.
6
+
7
+ Current package version: `0.1.2`.
5
8
 
6
9
  ## Requirements
7
10
 
8
- - Node.js 22.13 or newer.
9
- - A customer management API implementing the contract below.
11
+ - Node.js 22.13 or newer when running from source, npm, or npx.
12
+ - pnpm for local development and release builds.
13
+ - A RequestShield customer management API.
14
+ - Windows x64 and Inno Setup 6 when building the Windows installer.
10
15
 
11
- The API defaults to `https://api.intellifend.ai`. For development, set
12
- `REQUESTSHIELD_API_URL` to the management API origin.
16
+ The management API defaults to `https://api.intellifend.ai`. During local
17
+ development, set `REQUESTSHIELD_API_URL` to another API origin when needed.
13
18
 
14
- ## Commands
19
+ ## Implemented commands
15
20
 
16
21
  ```console
17
22
  requestshield signin
23
+ requestshield keys create [--yes]
24
+ requestshield agent setup --agent codex [--force]
25
+ requestshield update check
26
+ requestshield --help
27
+ requestshield --version
28
+ ```
29
+
30
+ The full command catalog shown by `requestshield --help` includes planned
31
+ commands. Only the commands listed above are currently implemented.
32
+
33
+ ### Sign in
34
+
35
+ ```console
36
+ requestshield signin
37
+ ```
38
+
39
+ Starts the device authorization flow. The CLI prints the verification URL and
40
+ user code but does not open a browser automatically. After authorization, the
41
+ session is saved in the current user's application-data directory.
42
+
43
+ For CI or automation, `REQUESTSHIELD_ACCESS_TOKEN` may provide the access token.
44
+ An environment token is never persisted by the CLI.
45
+
46
+ ### Create new keys
47
+
48
+ ```console
18
49
  requestshield keys create
50
+ ```
51
+
52
+ Asks the customer to type `ROTATE`, then atomically deactivates the previous
53
+ credentials and creates a new `appKey` and `apiSecret`.
54
+
55
+ - `appKey` identifies the application and may be used by the client integration.
56
+ - `apiSecret` is a backend-only secret, is displayed once, and is not saved by
57
+ the CLI.
58
+ - `--yes` skips the interactive confirmation and is intended for automation.
59
+
60
+ Store the Secret Key in a backend secret manager immediately after creation.
61
+
62
+ ### Install the Codex Skill
63
+
64
+ ```console
19
65
  requestshield agent setup --agent codex
20
66
  ```
21
67
 
22
- `signin` starts a device authorization flow and prints the verification URL and
23
- code for the customer to open manually. The returned session is stored under the user's standard
24
- application-data directory with permissions restricted to that user. CI may
25
- instead provide `REQUESTSHIELD_ACCESS_TOKEN`; environment tokens are never
26
- persisted.
68
+ Installs the bundled RequestShield Skill into
69
+ `$CODEX_HOME/skills/requestshield`, or `~/.codex/skills/requestshield` when
70
+ `CODEX_HOME` is not set. Restart Codex or start a new task after installation.
27
71
 
28
- `keys create` asks for confirmation, then atomically deactivates the previous
29
- application credentials and creates a new `appKey` + `apiSecret`. The public
30
- app key and the backend-only secret are printed after success. The secret is
31
- not stored by the CLI and cannot be retrieved again.
72
+ If that Skill already exists, the command preserves it. Use `--force` to
73
+ replace the existing copy:
32
74
 
33
- `agent setup --agent codex` installs the bundled `requestshield` skill into
34
- `$CODEX_HOME/skills`, or `~/.codex/skills` when `CODEX_HOME` is unset. Existing
35
- content is preserved unless `--force` is supplied.
75
+ ```console
76
+ requestshield agent setup --agent codex --force
77
+ ```
36
78
 
37
- ## Management API contract
79
+ ### Check for updates
38
80
 
39
- ### Start sign-in
81
+ ```console
82
+ requestshield update check
83
+ ```
40
84
 
41
- `POST /v1/cli/signin`
85
+ Reads the latest `requestshield` version from the npm registry. If a newer
86
+ version exists, the CLI displays the current and latest versions, asks the user
87
+ to confirm with `y`, then runs:
42
88
 
43
- ```json
44
- {
45
- "client": "requestshield-cli"
46
- }
89
+ ```console
90
+ npm install --global requestshield@<latest-version>
47
91
  ```
48
92
 
49
- The response contains `deviceCode`, `userCode`, `verificationUri`,
50
- `expiresIn`, and `interval`.
93
+ This updater is for npm global installations and requires `node` and `npm` on
94
+ the customer's machine. It does not replace an executable installed by the
95
+ Windows Inno Setup installer.
51
96
 
52
- ### Poll sign-in
97
+ ## Install and run with npm
53
98
 
54
- `POST /v1/cli/signin/token` with `{ "deviceCode": "..." }`.
99
+ Install globally so `requestshield` is available from any directory:
55
100
 
56
- Pending responses use HTTP 400 with `code` set to `authorization_pending` or
57
- `slow_down`. A successful response contains `accessToken` and optional account
58
- metadata.
101
+ ```console
102
+ npm install --global requestshield
103
+ requestshield --help
104
+ ```
59
105
 
60
- ### Rotate keys
106
+ Run without a global installation:
61
107
 
62
- `POST /v1/cli/keys/rotate` with Bearer authentication and an empty JSON body.
63
- The response contains `appKey` and one-time `apiSecret`.
108
+ ```console
109
+ npx --yes requestshield@0.1.2 --help
110
+ npx --yes requestshield@0.1.2 signin
111
+ ```
64
112
 
65
- ## Development
113
+ ## Local development
114
+
115
+ From the `requestshield-cli` directory:
66
116
 
67
117
  ```console
68
- pnpm --filter requestshield lint
69
- pnpm --filter requestshield typecheck
70
- pnpm --filter requestshield test
71
- pnpm --filter requestshield build
118
+ pnpm install
119
+ pnpm lint
120
+ pnpm typecheck
121
+ pnpm test
122
+ pnpm build
123
+ node src/main.mjs --help
124
+ node src/main.mjs --version
125
+ ```
126
+
127
+ `build` currently validates the syntax of all `.mjs` files. It does not create
128
+ the standalone Windows executable.
129
+
130
+ To test the update interaction without publishing or installing anything:
131
+
132
+ ```powershell
133
+ $env:REQUESTSHIELD_MOCK_LATEST_VERSION = "0.1.3"
134
+ pnpm test:update-check:manual
135
+ Remove-Item Env:REQUESTSHIELD_MOCK_LATEST_VERSION
72
136
  ```
73
- ## Quick start
74
137
 
75
- RequestShield requires Node.js 22.13 or newer.
138
+ The manual test prints `[MOCK] Would install ...`; it intentionally does not
139
+ change the installed CLI version.
140
+
141
+ ## Build the standalone Windows executable
142
+
143
+ The project uses Node.js Single Executable Applications (SEA). Run these steps
144
+ from the `requestshield-cli` directory in PowerShell:
145
+
146
+ ```powershell
147
+ New-Item -ItemType Directory -Force -Path .\dist | Out-Null
148
+ pnpm exec esbuild .\src\main.mjs --bundle --platform=node --format=cjs --target=node22 --outfile=.\dist\requestshield.cjs
149
+ node --experimental-sea-config .\sea-config.json
150
+ Copy-Item (Get-Command node.exe).Source .\dist\requestshield.exe -Force
151
+ pnpm exec postject .\dist\requestshield.exe NODE_SEA_BLOB .\dist\requestshield.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
152
+ .\dist\requestshield.exe --help
153
+ ```
154
+
155
+ The generated file is `dist\requestshield.exe`. It contains Node.js and does
156
+ not require Node.js to be installed on the customer's machine.
157
+
158
+ ## Build the Windows installer
76
159
 
77
- Run without installing globally:
160
+ Install Inno Setup 6, then compile the installer and pass the current package
161
+ version explicitly:
162
+
163
+ ```powershell
164
+ New-Item -ItemType Directory -Force -Path .\release | Out-Null
165
+ & "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" "/DAppVersion=0.1.2" "/FRequestShieldSetup-0.1.2-windows-x64" ".\installer\requestshield.iss"
166
+ ```
167
+
168
+ The installer is written to `release\RequestShieldSetup-0.1.2-windows-x64.exe`.
169
+ It installs `requestshield.exe` under the current user's local application-data
170
+ directory and adds that directory to the user `PATH`. The customer must open a
171
+ new terminal before running:
78
172
 
79
173
  ```console
80
- npx requestshield --help
81
- npx requestshield signin
82
- npx requestshield keys create
83
- npx requestshield agent setup --agent codex
174
+ requestshield --help
84
175
  ```
85
176
 
86
- Pin a specific version for reproducible usage:
177
+ ## Publish to npm
178
+
179
+ Before publishing, update `version` in `package.json`, run the checks, and
180
+ inspect the package contents:
87
181
 
88
182
  ```console
89
- npx requestshield@0.1.0 --help
183
+ pnpm lint
184
+ pnpm typecheck
185
+ pnpm test
186
+ pnpm pack --dry-run
187
+ pnpm publish --access public --no-git-checks
90
188
  ```
91
189
 
92
- To automatically accept npm's installation prompt:
190
+ npm may ask for a one-time password in the interactive terminal. After
191
+ publishing, verify the registry state:
93
192
 
94
193
  ```console
95
- npx --yes requestshield@0.1.0 --help
194
+ npm view requestshield version
195
+ npm dist-tag ls requestshield
196
+ npx --yes requestshield@0.1.2 --version
197
+ ```
198
+
199
+ ## Management API contract
200
+
201
+ ### Start sign-in
202
+
203
+ `POST /v1/cli/signin`
204
+
205
+ ```json
206
+ {
207
+ "client": "requestshield-cli"
208
+ }
96
209
  ```
210
+
211
+ The response contains `deviceCode`, `userCode`, `verificationUri`, `expiresIn`,
212
+ and `interval`.
213
+
214
+ ### Poll sign-in
215
+
216
+ `POST /v1/cli/signin/token` with `{ "deviceCode": "..." }`.
217
+
218
+ Pending responses use HTTP 400 with `code` set to `authorization_pending` or
219
+ `slow_down`. A successful response contains `accessToken` and optional account
220
+ metadata.
221
+
222
+ ### Rotate keys
223
+
224
+ `POST /v1/cli/keys/rotate` with Bearer authentication and an empty JSON body.
225
+ The response contains `appKey` and the one-time `apiSecret`.
226
+
227
+ ## Security notes
228
+
229
+ - Never place an access token or Secret Key in URLs, command arguments, logs,
230
+ fixtures, or committed files.
231
+ - The CLI does not persist the Secret Key returned by `keys create`.
232
+ - Sign the Windows executable and installer before distributing them to
233
+ customers, and publish a checksum through a trusted release channel.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "requestshield",
3
- "version": "0.1.0",
4
- "description": "Customer CLI for IntelliFend RequestShield.",
3
+ "version": "0.1.2",
4
+ "description": "Customer CLI for RequestShield.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "requestshield": "src/main.mjs"
@@ -14,18 +14,18 @@
14
14
  "engines": {
15
15
  "node": ">=22.13"
16
16
  },
17
- "scripts": {
18
- "build": "node scripts/check-package.mjs",
19
- "lint": "node scripts/check-package.mjs",
20
- "test": "node --test",
21
- "typecheck": "tsc -p jsconfig.json --noEmit",
22
- "prepublishOnly": "npm run lint && npm run typecheck && npm test"
23
- },
24
17
  "devDependencies": {
25
18
  "@types/node": "26.1.1",
26
19
  "esbuild": "^0.28.1",
27
20
  "postject": "1.0.0-alpha.6",
28
21
  "typescript": "^6.0.3"
29
22
  },
30
- "license": "UNLICENSED"
31
- }
23
+ "license": "UNLICENSED",
24
+ "scripts": {
25
+ "build": "node scripts/check-package.mjs",
26
+ "lint": "node scripts/check-package.mjs",
27
+ "test": "node --test",
28
+ "test:update-check:manual": "node scripts/manual-update-check.mjs",
29
+ "typecheck": "tsc -p jsconfig.json --noEmit"
30
+ }
31
+ }
package/src/args.mjs CHANGED
@@ -2,18 +2,61 @@
2
2
 
3
3
  import { CliError } from "./errors.mjs";
4
4
 
5
- const HELP = `RequestShield customer CLI
5
+ const HELP = `RequestShield CLI
6
6
 
7
7
  Usage:
8
- requestshield signin
9
- requestshield keys create [--yes]
10
- requestshield agent setup --agent codex [--force]
11
- requestshield --help
12
- requestshield --version`;
8
+ requestshield [command] [options]
9
+
10
+ Commands:
11
+ update
12
+ check Check for and install the latest CLI version
13
+
14
+ auth
15
+ signin Sign in to RequestShield
16
+ status Show authentication status
17
+
18
+ keys
19
+ create Create a new application and key pair
20
+ deactivate Deactivate application credentials
21
+ rotate Rotate the Secret Key for an application
22
+
23
+ agent
24
+ setup Install the RequestShield Agent Skill
25
+ setup --agent codex Install the Skill for Codex
26
+ setup --agent claude Install the Skill for Claude
27
+ setup --status Show Agent Skill installation status
28
+
29
+ contract
30
+ get Get the RequestShield integration contract
31
+
32
+ apps
33
+ list List accessible applications
34
+ get <app-key> Get application information
35
+ readiness <app-key> Check application readiness
36
+
37
+ credentials
38
+ status <app-key> Show credential status without exposing secrets
39
+
40
+ challenge
41
+ volume <app-key> Get challenge volume for an application
42
+
43
+ billing
44
+ get <app-key> Get billing and plan information
45
+
46
+ protect
47
+ list <app-key> List protected endpoints
48
+ add Add a protected endpoint
49
+ remove Remove a protected endpoint
50
+
51
+ Options:
52
+ -h, --help Show help
53
+ -v, --version Show CLI version
54
+ --json Output machine-readable JSON`;
13
55
 
14
56
  /**
15
57
  * @typedef {{ command: "help", help: string }
16
58
  * | { command: "version" }
59
+ * | { command: "update-check" }
17
60
  * | { command: "signin" }
18
61
  * | { command: "keys-create", yes: boolean }
19
62
  * | { command: "agent-setup", agent: "codex", force: boolean }} ParsedArgs
@@ -24,9 +67,13 @@ export function parseArgs(argv) {
24
67
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
25
68
  return { command: "help", help: HELP };
26
69
  }
27
- if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-V")) {
70
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V")) {
28
71
  return { command: "version" };
29
72
  }
73
+ if (argv[0] === "update" && argv[1] === "check") {
74
+ assertOnly(argv.slice(2), new Set());
75
+ return { command: "update-check" };
76
+ }
30
77
  if (argv[0] === "signin") {
31
78
  assertOnly(argv.slice(1), new Set());
32
79
  return { command: "signin" };
package/src/cli.mjs CHANGED
@@ -7,17 +7,28 @@ import { SessionStore } from "./session-store.mjs";
7
7
  import { signin } from "./commands/signin.mjs";
8
8
  import { createKeys } from "./commands/keys-create.mjs";
9
9
  import { setupAgent } from "./commands/agent-setup.mjs";
10
+ import { checkForUpdate } from "./commands/update-check.mjs";
10
11
  import packageJson from "../package.json" with { type: "json" };
11
12
 
12
13
  /**
13
14
  * @param {string[]} argv
14
- * @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean> }} [deps]
15
+ * @param {{ log?: (message: string) => void, api?: ManagementApiClient, sessions?: SessionStore, env?: NodeJS.ProcessEnv, homeDir?: string, sourceDir?: string, wait?: (milliseconds: number) => Promise<unknown>, confirm?: () => Promise<boolean>, fetchImpl?: typeof fetch, confirmUpdate?: () => Promise<boolean>, installLatest?: (packageName: string, version: string) => Promise<void> }} [deps]
15
16
  */
16
17
  export async function run(argv, deps = {}) {
17
18
  const parsed = parseArgs(argv);
18
19
  const log = deps.log ?? ((message) => console.log(message));
19
20
  if (parsed.command === "help") return log(parsed.help);
20
- if (parsed.command === "version") return log(packageJson.version);
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
+ }
21
32
 
22
33
  if (parsed.command === "agent-setup") {
23
34
  return setupAgent(parsed, { log, ...deps });
@@ -0,0 +1,139 @@
1
+ // @ts-check
2
+
3
+ import readline from "node:readline/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { stdin, stdout } from "node:process";
6
+ import { CliError } from "../errors.mjs";
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
9
+
10
+ /**
11
+ * @param {{ currentVersion: string, packageName: string, log: (message: string) => void, fetchImpl?: typeof fetch, confirm?: () => Promise<boolean>, install?: (packageName: string, version: string) => Promise<void> }} options
12
+ */
13
+ export async function checkForUpdate(options) {
14
+ const latestVersion = await fetchLatestVersion(
15
+ options.packageName,
16
+ options.fetchImpl ?? fetch,
17
+ );
18
+
19
+ options.log(`Current version: ${options.currentVersion}`);
20
+ options.log(`Latest version: ${latestVersion}`);
21
+
22
+ if (compareVersions(latestVersion, options.currentVersion) <= 0) {
23
+ options.log("RequestShield is already up to date.");
24
+ return;
25
+ }
26
+
27
+ const accepted = await (options.confirm ?? confirmUpdate)(latestVersion);
28
+ if (!accepted) {
29
+ options.log("Update cancelled.");
30
+ return;
31
+ }
32
+
33
+ options.log(`Updating RequestShield to ${latestVersion}...`);
34
+ await (options.install ?? installWithNpm)(options.packageName, latestVersion);
35
+ options.log(`RequestShield ${latestVersion} was installed successfully.`);
36
+ options.log("Open a new terminal before running RequestShield again.");
37
+ }
38
+
39
+ /** @param {string} packageName @param {typeof fetch} fetchImpl */
40
+ async function fetchLatestVersion(packageName, fetchImpl) {
41
+ const url = `${DEFAULT_REGISTRY}/${encodeURIComponent(packageName)}/latest`;
42
+ let response;
43
+ try {
44
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
45
+ } catch (error) {
46
+ throw new CliError(`Could not check the latest RequestShield version: ${messageOf(error)}`, {
47
+ code: "UPDATE_CHECK_FAILED",
48
+ exitCode: 7,
49
+ });
50
+ }
51
+
52
+ if (!response.ok) {
53
+ throw new CliError(`Could not check the latest RequestShield version: HTTP ${response.status}`, {
54
+ code: "UPDATE_CHECK_FAILED",
55
+ exitCode: 7,
56
+ });
57
+ }
58
+
59
+ const body = await response.json();
60
+ const version = body && typeof body === "object" ? Reflect.get(body, "version") : undefined;
61
+ if (typeof version !== "string" || !parseVersion(version)) {
62
+ throw new CliError("The npm registry returned an invalid RequestShield version", {
63
+ code: "INVALID_UPDATE_RESPONSE",
64
+ });
65
+ }
66
+ return version;
67
+ }
68
+
69
+ /** @param {string} latestVersion */
70
+ async function confirmUpdate(latestVersion) {
71
+ if (!stdin.isTTY || !stdout.isTTY) {
72
+ throw new CliError(`RequestShield ${latestVersion} is available, but confirmation requires an interactive terminal`, {
73
+ code: "UPDATE_CONFIRMATION_REQUIRED",
74
+ exitCode: 2,
75
+ });
76
+ }
77
+ const prompt = readline.createInterface({ input: stdin, output: stdout });
78
+ try {
79
+ const answer = await prompt.question(`Update to RequestShield ${latestVersion}? (y/N): `);
80
+ return answer.trim().toLowerCase() === "y";
81
+ } finally {
82
+ prompt.close();
83
+ }
84
+ }
85
+
86
+ /** @param {string} packageName @param {string} version */
87
+ async function installWithNpm(packageName, version) {
88
+ await new Promise((resolve, reject) => {
89
+ const child = spawn("npm", ["install", "--global", `${packageName}@${version}`], {
90
+ shell: process.platform === "win32",
91
+ stdio: "inherit",
92
+ windowsHide: true,
93
+ });
94
+ child.once("error", (error) => {
95
+ reject(new CliError(`Could not start npm: ${error.message}`, {
96
+ code: "UPDATE_FAILED",
97
+ exitCode: 1,
98
+ }));
99
+ });
100
+ child.once("exit", (code) => {
101
+ if (code === 0) resolve(undefined);
102
+ else reject(new CliError(`npm update failed with exit code ${code ?? "unknown"}`, {
103
+ code: "UPDATE_FAILED",
104
+ exitCode: 1,
105
+ }));
106
+ });
107
+ });
108
+ }
109
+
110
+ /** @param {string} left @param {string} right */
111
+ function compareVersions(left, right) {
112
+ const a = parseVersion(left);
113
+ const b = parseVersion(right);
114
+ if (!a || !b) throw new CliError("RequestShield has an invalid semantic version");
115
+ for (let index = 0; index < 3; index++) {
116
+ if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] - b.numbers[index];
117
+ }
118
+ if (a.prerelease === b.prerelease) return 0;
119
+ if (a.prerelease === undefined) return 1;
120
+ if (b.prerelease === undefined) return -1;
121
+ return a.prerelease.localeCompare(b.prerelease, "en", { numeric: true });
122
+ }
123
+
124
+ /** @param {string} value */
125
+ function parseVersion(value) {
126
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
127
+ if (!match) return undefined;
128
+ return {
129
+ numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
130
+ prerelease: match[4],
131
+ };
132
+ }
133
+
134
+ /** @param {unknown} error */
135
+ function messageOf(error) {
136
+ return error instanceof Error ? error.message : String(error);
137
+ }
138
+
139
+ export { compareVersions };
package/src/main.mjs CHANGED
File without changes