requestshield 0.1.7 → 0.1.9
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 +167 -58
- package/config/.env.prod +4 -0
- package/package.json +1 -1
- package/skills/requestshield/SKILL.md +20 -17
- package/skills/requestshield/assets/AGENTS.codex.md +5 -3
- 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 +1 -1
- package/skills/requestshield/references/cli.md +92 -28
- package/skills/requestshield/references/integration-planning.md +9 -7
- package/skills/requestshield/references/troubleshooting.md +2 -2
- package/src/api-client.mjs +1 -1
- package/src/args.mjs +98 -106
- package/src/cli.mjs +69 -243
- package/src/command-registry.mjs +97 -0
- package/src/commands/agent-setup.mjs +24 -17
- package/src/commands/agent-status.mjs +60 -0
- package/src/commands/application-mutations.mjs +8 -4
- package/src/commands/apps-get.mjs +12 -4
- package/src/commands/apps-list.mjs +20 -13
- package/src/commands/contract.mjs +25 -0
- package/src/commands/keys-create.mjs +5 -2
- package/src/commands/mutation-support.mjs +26 -12
- package/src/commands/output.mjs +21 -0
- package/src/commands/secret-commands.mjs +13 -6
- package/src/commands/signin.mjs +17 -8
- package/src/commands/signout.mjs +7 -3
- package/src/commands/update-check.mjs +97 -43
- package/src/config.mjs +29 -3
- package/src/entrypoint.mjs +20 -13
- package/src/integration-contract-client.mjs +81 -0
- package/src/integration-contract.mjs +104 -0
- package/src/oauth-client.mjs +2 -2
- package/src/oauth-loopback.mjs +1 -1
- package/src/session-files.mjs +4 -4
- package/src/session-store.mjs +2 -2
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { parseApplicationResponse } from "./application-response.mjs";
|
|
3
|
+
import { outputContext, printApplication } from "./output.mjs";
|
|
3
4
|
import { confirmAction, mutationKey, parseAccepted, printAccepted, validateHttpStatus, withMutationRecovery } from "./mutation-support.mjs";
|
|
4
5
|
|
|
5
6
|
/** @typedef {import('./mutation-support.mjs').MutationResponse} MutationResponse */
|
|
6
7
|
/** @typedef {import('./mutation-support.mjs').CommandOutput & {sessions: {loadToken(): Promise<string>}}} Dependencies */
|
|
7
8
|
|
|
8
|
-
/** @param {{appKey: string, name: string, yes?: boolean, idempotencyKey?: string}} options
|
|
9
|
+
/** @param {{appKey: string, name: string, json?: boolean, yes?: boolean, idempotencyKey?: string}} options
|
|
9
10
|
* @param {Dependencies & {api: {renameApp(token: string, appKey: string, options: {name: string, idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
10
11
|
*/
|
|
11
12
|
export async function renameApp(options, deps) {
|
|
13
|
+
deps = outputContext(options, deps);
|
|
12
14
|
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
13
15
|
const token = await deps.sessions.loadToken();
|
|
14
16
|
const application = await withMutationRecovery(idempotencyKey, deps, async () => {
|
|
@@ -16,18 +18,20 @@ export async function renameApp(options, deps) {
|
|
|
16
18
|
validateHttpStatus(result, 200);
|
|
17
19
|
return parseApplicationResponse(result.body, options.appKey);
|
|
18
20
|
});
|
|
19
|
-
|
|
21
|
+
printApplication(application, options, deps);
|
|
22
|
+
return application;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
/** @param {{appKey: string, enabled: boolean, yes?: boolean, idempotencyKey?: string}} options
|
|
25
|
+
/** @param {{appKey: string, enabled: boolean, json?: boolean, yes?: boolean, idempotencyKey?: string}} options
|
|
23
26
|
* @param {Dependencies & {api: {setAppEnabled(token: string, appKey: string, options: {enabled: boolean, idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
24
27
|
*/
|
|
25
28
|
export async function setAppEnabled(options, deps) {
|
|
29
|
+
deps = outputContext(options, deps);
|
|
26
30
|
if (!options.enabled) await confirmAction(options, deps, `Disable application ${options.appKey}?`, "DISABLE");
|
|
27
31
|
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
28
32
|
const token = await deps.sessions.loadToken();
|
|
29
33
|
await withMutationRecovery(idempotencyKey, deps, async () => {
|
|
30
34
|
parseAccepted(await deps.api.setAppEnabled(token, options.appKey, {enabled: options.enabled, idempotencyKey}));
|
|
31
35
|
});
|
|
32
|
-
printAccepted(options.enabled ? "Application enable" : "Application disable", options.appKey, deps);
|
|
36
|
+
return printAccepted(options.enabled ? "Application enable" : "Application disable", options.appKey, deps, options);
|
|
33
37
|
}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { parseApplicationResponse } from "./application-response.mjs";
|
|
4
|
+
import { printApplication } from "./output.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* @param {{ appKey: string }} options
|
|
7
|
+
* @param {{ appKey: string, json?: boolean }} options
|
|
7
8
|
* @param {{ api: { getApp(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
|
|
8
9
|
*/
|
|
9
10
|
export async function getApp(options, deps) {
|
|
11
|
+
const app = await fetchApp(options.appKey, deps);
|
|
12
|
+
printApplication(app, options, deps);
|
|
13
|
+
return app;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {string} appKey @param {Pick<Parameters<typeof getApp>[1], 'api' | 'sessions'>} deps */
|
|
17
|
+
export async function fetchApp(appKey, deps) {
|
|
10
18
|
const accessToken =
|
|
11
19
|
await deps.sessions.loadToken();
|
|
12
20
|
|
|
13
21
|
const result =
|
|
14
|
-
await deps.api.getApp(accessToken,
|
|
22
|
+
await deps.api.getApp(accessToken, appKey);
|
|
15
23
|
|
|
16
24
|
const app =
|
|
17
|
-
parseApplicationResponse(result.body,
|
|
25
|
+
parseApplicationResponse(result.body, appKey);
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
return app;
|
|
20
28
|
}
|
|
@@ -14,6 +14,25 @@ const MAX_PAGES = 100;
|
|
|
14
14
|
* @param {{api: {listApps(accessToken: string, options: {limit?: number, cursor?: string}): Promise<{body: unknown}>}, sessions: {loadToken(): Promise<string>}, log: (message: string) => void, profile?: import('../config.mjs').Profile}} deps
|
|
15
15
|
*/
|
|
16
16
|
export async function listApps(options, deps) {
|
|
17
|
+
const result = await collectApps(options, deps);
|
|
18
|
+
const {data: apps, nextCursor} = result;
|
|
19
|
+
if (options.json) deps.log(JSON.stringify(result, null, 2));
|
|
20
|
+
else {
|
|
21
|
+
if (apps.length === 0) {
|
|
22
|
+
deps.log(nextCursor === null && !options.cursor
|
|
23
|
+
? "No applications are available to this account."
|
|
24
|
+
: "No applications were returned on this page.");
|
|
25
|
+
} else {
|
|
26
|
+
for (const line of formatTable(apps)) deps.log(line);
|
|
27
|
+
}
|
|
28
|
+
if (nextCursor !== null) deps.log(`More applications are available. Continue with \`${getCommandInvocation(deps.profile)} app list --cursor ${nextCursor}\` or use --all.`);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {{limit?: number, cursor?: string, all?: boolean}} options
|
|
34
|
+
* @param {Pick<Parameters<typeof listApps>[1], 'api' | 'sessions'>} deps */
|
|
35
|
+
export async function collectApps(options, deps) {
|
|
17
36
|
/** @type {App[]} */
|
|
18
37
|
const apps = [];
|
|
19
38
|
let cursor = options.cursor;
|
|
@@ -41,19 +60,7 @@ export async function listApps(options, deps) {
|
|
|
41
60
|
}
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
deps.log(JSON.stringify({ data: apps, nextCursor }, null, 2));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (apps.length === 0) {
|
|
50
|
-
deps.log(nextCursor === null && !options.cursor
|
|
51
|
-
? "No applications are available to this account."
|
|
52
|
-
: "No applications were returned on this page.");
|
|
53
|
-
} else {
|
|
54
|
-
for (const line of formatTable(apps)) deps.log(line);
|
|
55
|
-
}
|
|
56
|
-
if (nextCursor !== null) deps.log(`More applications are available. Continue with \`${getCommandInvocation(deps.profile)} apps list --cursor ${nextCursor}\` or use --all.`);
|
|
63
|
+
return {data: apps, nextCursor};
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/** @param {unknown} body @returns {{ data: App[], nextCursor: string | null }} */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getDocsUrl } from "../config.mjs";
|
|
3
|
+
import { IntegrationContractClient } from "../integration-contract-client.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{json?: boolean}} options
|
|
7
|
+
* @param {{profile?: import('../config.mjs').Profile, fetchImpl?: typeof fetch, log(message: string): void}} deps
|
|
8
|
+
*/
|
|
9
|
+
export async function showContract(options, deps) {
|
|
10
|
+
const profile = deps.profile ?? "prod";
|
|
11
|
+
const contract = await new IntegrationContractClient({
|
|
12
|
+
docsUrl: getDocsUrl(profile), environment: profile, fetchImpl: deps.fetchImpl,
|
|
13
|
+
}).getContract();
|
|
14
|
+
if (options.json) deps.log(JSON.stringify(contract, null, 2));
|
|
15
|
+
else {
|
|
16
|
+
const {data} = contract;
|
|
17
|
+
deps.log(`Integration contract: ${data.contractVersion} (schema ${data.schemaVersion})\nEnvironment: ${data.environment}`);
|
|
18
|
+
deps.log(`Challenge service: ${data.services.challengeUrl}`);
|
|
19
|
+
deps.log(`Browser SDK: ${data.browser.sdkVersion}\nScript: ${data.browser.scriptUrl}\nIntegrity: ${data.browser.integrity}\nToken header: ${data.browser.tokenHeader}\nModes: ${data.browser.availableModes.join(", ")}`);
|
|
20
|
+
deps.log(`CSP additions:\n script-src: ${data.browser.cspAdditions.scriptSrc.join(" ")}\n connect-src: ${data.browser.cspAdditions.connectSrc.join(" ")}\n worker-src: ${data.browser.cspAdditions.workerSrc.join(" ")}`);
|
|
21
|
+
deps.log(`Java SDK: ${data.backend.sdkVersion} (JDK ${data.backend.minJdk}+)\nMaven repository: ${data.backend.mavenRepository}\nCore: ${data.backend.core.groupId}:${data.backend.core.artifactId}:${data.backend.sdkVersion}\n${data.backend.springBoot3.framework}: ${data.backend.springBoot3.groupId}:${data.backend.springBoot3.artifactId}:${data.backend.sdkVersion}`);
|
|
22
|
+
deps.log(`Documentation:\n Browser SDK: ${data.documentation.browserSdk}\n Java SDK: ${data.documentation.javaSdk}\n Domains and CSP: ${data.documentation.domainsAndCsp}`);
|
|
23
|
+
}
|
|
24
|
+
return contract;
|
|
25
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { mutationKey, parseIssuance, printIssuance, withMutationRecovery } from "./mutation-support.mjs";
|
|
4
|
+
import { outputContext } from "./output.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* @param {{appName: string, yes?: boolean, idempotencyKey?: string}} options
|
|
7
|
+
* @param {{appName: string, json?: boolean, yes?: boolean, idempotencyKey?: string}} options
|
|
7
8
|
* @param {import('./mutation-support.mjs').CommandOutput & {api: {createApp(token: string, options: {name: string, idempotencyKey: string}): Promise<import('./mutation-support.mjs').MutationResponse>}, sessions: {loadToken(): Promise<string>}}} deps
|
|
8
9
|
*/
|
|
9
10
|
export async function createKeys(options, deps) {
|
|
11
|
+
deps = outputContext(options, deps);
|
|
10
12
|
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
11
13
|
const token = await deps.sessions.loadToken();
|
|
12
14
|
const issuance = await withMutationRecovery(idempotencyKey, deps, async () =>
|
|
13
15
|
parseIssuance(await deps.api.createApp(token, {name: options.appName, idempotencyKey}), 201));
|
|
14
|
-
printIssuance(issuance, deps);
|
|
16
|
+
printIssuance(issuance, deps, options);
|
|
17
|
+
return issuance;
|
|
15
18
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import readline from "node:readline/promises";
|
|
4
|
-
import { stdin,
|
|
4
|
+
import { stdin, stderr } from "node:process";
|
|
5
|
+
import { printData } from "./output.mjs";
|
|
5
6
|
import { CliError } from "../errors.mjs";
|
|
6
7
|
import { getCommandInvocation } from "../config.mjs";
|
|
7
8
|
import { APPLICATION_STATUSES, invalidResponse, responseObject } from "./application-response.mjs";
|
|
@@ -18,16 +19,17 @@ export function mutationKey(supplied) {
|
|
|
18
19
|
return key;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/** @param {{yes?: boolean}} options @param {CommandOutput} deps @param {string} prompt @param {string} word */
|
|
22
|
+
/** @param {{yes?: boolean, json?: boolean}} options @param {CommandOutput} deps @param {string} prompt @param {string} word */
|
|
22
23
|
export async function confirmAction(options, deps, prompt, word) {
|
|
23
24
|
if (options.yes) return;
|
|
25
|
+
if (options.json) throw new CliError("This action requires confirmation; use --yes with --json", {code: "CONFIRMATION_REQUIRED", exitCode: 2});
|
|
24
26
|
let accepted;
|
|
25
27
|
if (deps.confirm) accepted = await deps.confirm();
|
|
26
28
|
else {
|
|
27
|
-
if (!stdin.isTTY || !
|
|
29
|
+
if (!stdin.isTTY || !stderr.isTTY) {
|
|
28
30
|
throw new CliError("This action requires confirmation; use --yes in a non-interactive terminal", {code: "CONFIRMATION_REQUIRED", exitCode: 2});
|
|
29
31
|
}
|
|
30
|
-
const terminal = readline.createInterface({input: stdin, output:
|
|
32
|
+
const terminal = readline.createInterface({input: stdin, output: stderr});
|
|
31
33
|
try { accepted = await terminal.question(`${prompt} Type ${word} to continue: `) === word; }
|
|
32
34
|
finally { terminal.close(); }
|
|
33
35
|
}
|
|
@@ -81,18 +83,26 @@ export function isApiSecret(value) {
|
|
|
81
83
|
return typeof value === "string" && /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(value);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
/** @param {{appKey: string, status: string, apiSecret: string | null}} issuance @param {CommandOutput} deps */
|
|
85
|
-
export function printIssuance(issuance, deps) {
|
|
86
|
+
/** @param {{appKey: string, status: string, apiSecret: string | null}} issuance @param {CommandOutput} deps @param {{json?: boolean}} [options] */
|
|
87
|
+
export function printIssuance(issuance, deps, options = {}) {
|
|
86
88
|
const invocation = getCommandInvocation(deps.profile);
|
|
89
|
+
if (options.json) {
|
|
90
|
+
printData(issuance, deps);
|
|
91
|
+
const warn = deps.warn ?? console.error;
|
|
92
|
+
if (issuance.apiSecret === null) warn(`This is an idempotency replay; the API did not return a secret. Run \`${invocation} secret reveal ${issuance.appKey}\` to retrieve the current active secret.`);
|
|
93
|
+
else warn("Store this secret in your backend secret manager. The CLI did not save it.");
|
|
94
|
+
(deps.warn ?? console.error)("Configuration publication is asynchronous; this response does not confirm propagation.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
87
97
|
deps.log(`App Key: ${issuance.appKey}`);
|
|
88
98
|
deps.log(`Status: ${issuance.status}`);
|
|
89
99
|
if (issuance.apiSecret === null) {
|
|
90
|
-
deps.log(`This is an idempotency replay; the API did not return a secret. Run \`${invocation}
|
|
100
|
+
deps.log(`This is an idempotency replay; the API did not return a secret. Run \`${invocation} secret reveal ${issuance.appKey}\` to retrieve the current active secret.`);
|
|
91
101
|
} else {
|
|
92
102
|
deps.log(`Secret Key: ${issuance.apiSecret}`);
|
|
93
103
|
deps.log("Store this secret in your backend secret manager. The CLI did not save it.");
|
|
94
104
|
}
|
|
95
|
-
deps.log("Configuration publication is asynchronous; this response does not confirm propagation.");
|
|
105
|
+
(deps.warn ?? deps.log)("Configuration publication is asynchronous; this response does not confirm propagation.");
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
/** @param {MutationResponse} result */
|
|
@@ -103,8 +113,12 @@ export function parseAccepted(result) {
|
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
/** @param {string} action @param {string} appKey @param {CommandOutput} deps */
|
|
107
|
-
export function printAccepted(action, appKey, deps) {
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
/** @param {string} action @param {string} appKey @param {CommandOutput} deps @param {{json?: boolean}} [options] */
|
|
117
|
+
export function printAccepted(action, appKey, deps, options = {}) {
|
|
118
|
+
const data = {appKey, status: "accepted"};
|
|
119
|
+
if (options.json) printData(data, deps);
|
|
120
|
+
else deps.log(`${action} accepted for ${appKey}.`);
|
|
121
|
+
(deps.warn ?? deps.log)("Configuration publication is asynchronous.");
|
|
122
|
+
(deps.warn ?? deps.log)(`Run \`${getCommandInvocation(deps.profile)} app get ${appKey}\` to check the current application status.`);
|
|
123
|
+
return data;
|
|
110
124
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @param {unknown} data @param {{log(message: string): void}} deps */
|
|
4
|
+
export function printData(data, deps) {
|
|
5
|
+
deps.log(JSON.stringify({data}, null, 2));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Keep diagnostics separate from machine-readable results, including direct callers.
|
|
9
|
+
* @template {{log: (message: string) => void, warn?: (message: string) => void}} T
|
|
10
|
+
* @param {{json?: boolean}} options @param {T} deps @returns {T}
|
|
11
|
+
*/
|
|
12
|
+
export function outputContext(options, deps) {
|
|
13
|
+
return {...deps, warn: deps.warn ?? (options.json ? console.error : deps.log)};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {import('./application-response.mjs').Application} app
|
|
17
|
+
* @param {{json?: boolean}} options @param {{log(message: string): void}} deps */
|
|
18
|
+
export function printApplication(app, options, deps) {
|
|
19
|
+
if (options.json) return printData(app, deps);
|
|
20
|
+
deps.log(`App Key: ${app.appKey}\nName: ${app.name}\nStatus: ${app.status}\nCreated: ${app.createdAt}\nUpdated: ${app.updatedAt}`);
|
|
21
|
+
}
|
|
@@ -1,45 +1,52 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { invalidResponse, responseObject } from "./application-response.mjs";
|
|
3
|
+
import { outputContext, printData } from "./output.mjs";
|
|
3
4
|
import { confirmAction, isApiSecret, mutationKey, parseAccepted, parseIssuance, printAccepted, printIssuance, validateHttpStatus, withMutationRecovery } from "./mutation-support.mjs";
|
|
4
5
|
|
|
5
6
|
/** @typedef {import('./mutation-support.mjs').MutationResponse} MutationResponse */
|
|
6
7
|
/** @typedef {import('./mutation-support.mjs').CommandOutput & {sessions: {loadToken(): Promise<string>}}} Dependencies */
|
|
7
|
-
/** @typedef {{appKey: string, yes?: boolean, idempotencyKey?: string}} MutationOptions */
|
|
8
|
+
/** @typedef {{appKey: string, yes?: boolean, json?: boolean, idempotencyKey?: string}} MutationOptions */
|
|
8
9
|
|
|
9
10
|
/** @param {MutationOptions} options
|
|
10
11
|
* @param {Dependencies & {api: {rotateSecret(token: string, appKey: string, options: {idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
11
12
|
*/
|
|
12
13
|
export async function rotateSecret(options, deps) {
|
|
14
|
+
deps = outputContext(options, deps);
|
|
13
15
|
await confirmAction(options, deps, `Replace the current secret for ${options.appKey}? Existing backend configuration will need the new secret.`, "ROTATE");
|
|
14
16
|
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
15
17
|
const token = await deps.sessions.loadToken();
|
|
16
18
|
const issuance = await withMutationRecovery(idempotencyKey, deps, async () =>
|
|
17
19
|
parseIssuance(await deps.api.rotateSecret(token, options.appKey, {idempotencyKey}), 200, options.appKey));
|
|
18
|
-
printIssuance(issuance, deps);
|
|
20
|
+
printIssuance(issuance, deps, options);
|
|
21
|
+
return issuance;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
/** @param {{appKey: string, yes?: boolean}} options
|
|
24
|
+
/** @param {{appKey: string, yes?: boolean, json?: boolean}} options
|
|
22
25
|
* @param {Dependencies & {api: {revealSecret(token: string, appKey: string): Promise<MutationResponse>}}} deps
|
|
23
26
|
*/
|
|
24
27
|
export async function revealSecret(options, deps) {
|
|
28
|
+
deps = outputContext(options, deps);
|
|
25
29
|
await confirmAction(options, deps, `Display the current API secret for ${options.appKey} in this terminal?`, "REVEAL");
|
|
26
30
|
const token = await deps.sessions.loadToken();
|
|
27
31
|
const result = await deps.api.revealSecret(token, options.appKey);
|
|
28
32
|
validateHttpStatus(result, 200);
|
|
29
33
|
const {apiSecret} = responseObject(result.body);
|
|
30
34
|
if (!isApiSecret(apiSecret)) throw invalidResponse("The reveal response did not contain a valid API secret");
|
|
31
|
-
|
|
32
|
-
deps.log(
|
|
35
|
+
if (options.json) printData({apiSecret}, deps);
|
|
36
|
+
else deps.log(`Secret Key: ${apiSecret}`);
|
|
37
|
+
(deps.warn ?? deps.log)("Store this secret in your backend secret manager. The CLI did not save it.");
|
|
38
|
+
return {apiSecret};
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/** @param {MutationOptions} options
|
|
36
42
|
* @param {Dependencies & {api: {revokeSecret(token: string, appKey: string, options: {idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
37
43
|
*/
|
|
38
44
|
export async function revokeSecret(options, deps) {
|
|
45
|
+
deps = outputContext(options, deps);
|
|
39
46
|
await confirmAction(options, deps, `Revoke the current API secret for ${options.appKey}?`, "REVOKE");
|
|
40
47
|
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
41
48
|
const token = await deps.sessions.loadToken();
|
|
42
49
|
await withMutationRecovery(idempotencyKey, deps, async () =>
|
|
43
50
|
parseAccepted(await deps.api.revokeSecret(token, options.appKey, {idempotencyKey})));
|
|
44
|
-
printAccepted("Secret revocation", options.appKey, deps);
|
|
51
|
+
return printAccepted("Secret revocation", options.appKey, deps, options);
|
|
45
52
|
}
|
package/src/commands/signin.mjs
CHANGED
|
@@ -4,12 +4,15 @@ import { CliError } from "../errors.mjs";
|
|
|
4
4
|
import { REQUESTED_OAUTH_SCOPES, validateAuthorizationIssuer } from "../config.mjs";
|
|
5
5
|
import { createLoopbackReceiver } from "../oauth-loopback.mjs";
|
|
6
6
|
import { openBrowser } from "../browser-opener.mjs";
|
|
7
|
+
import { printData, outputContext } from "./output.mjs";
|
|
7
8
|
|
|
8
9
|
/** @typedef {{
|
|
9
10
|
* oauth: Pick<import('../oauth-client.mjs').OAuthClient, 'discover' | 'exchangeCode' | 'config'>,
|
|
10
11
|
* authorizationIssuer?: string,
|
|
11
12
|
* sessions: {save(credentials: import('../oauth-client.mjs').OAuthCredentials): Promise<void>},
|
|
12
13
|
* log: (message: string) => void,
|
|
14
|
+
* warn?: (message: string) => void,
|
|
15
|
+
* profile?: import('../config.mjs').Profile,
|
|
13
16
|
* openBrowser?: typeof openBrowser,
|
|
14
17
|
* signal?: AbortSignal,
|
|
15
18
|
* timeoutMs?: number,
|
|
@@ -17,8 +20,11 @@ import { openBrowser } from "../browser-opener.mjs";
|
|
|
17
20
|
* }} SigninDependencies
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
|
-
/** @param {{noOpen?: boolean}} options @param {SigninDependencies} deps */
|
|
21
|
-
export async function signin(
|
|
23
|
+
/** @param {{noOpen?: boolean, json?: boolean}} options @param {SigninDependencies} deps */
|
|
24
|
+
export async function signin(options, deps) {
|
|
25
|
+
const {noOpen = false} = options;
|
|
26
|
+
deps = outputContext(options, deps);
|
|
27
|
+
const progress = deps.warn ?? deps.log;
|
|
22
28
|
const authorizationIssuer = validateAuthorizationIssuer(deps.authorizationIssuer ?? deps.oauth.config.issuer);
|
|
23
29
|
const controller = new AbortController();
|
|
24
30
|
let expired = false;
|
|
@@ -45,13 +51,13 @@ export async function signin({noOpen = false}, deps) {
|
|
|
45
51
|
code_challenge_method: "S256", state, scope: REQUESTED_OAUTH_SCOPES.join(" "),
|
|
46
52
|
}).toString();
|
|
47
53
|
if (noOpen) {
|
|
48
|
-
|
|
54
|
+
progress(`Open this authorization URL in a browser on this computer:\n${authorizeUrl.href}`);
|
|
49
55
|
} else {
|
|
50
|
-
|
|
56
|
+
progress("Opening your browser to sign in. Complete authorization on this computer.");
|
|
51
57
|
try { await (deps.openBrowser ?? openBrowser)(authorizeUrl.href, {signal}); }
|
|
52
58
|
catch {
|
|
53
59
|
signal.throwIfAborted();
|
|
54
|
-
|
|
60
|
+
progress(`Open this authorization URL in a browser on this computer:\n${authorizeUrl.href}`);
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
const {code} = await receiver.result;
|
|
@@ -61,14 +67,17 @@ export async function signin({noOpen = false}, deps) {
|
|
|
61
67
|
await deps.sessions.save(credentials);
|
|
62
68
|
signal.throwIfAborted();
|
|
63
69
|
receiver.complete(true);
|
|
64
|
-
|
|
70
|
+
const result = {profile: deps.profile ?? "prod", signedIn: true};
|
|
71
|
+
if (options.json) printData(result, deps);
|
|
72
|
+
else deps.log("Signed in successfully. The session is stored for the current user.");
|
|
73
|
+
return result;
|
|
65
74
|
} catch (error) {
|
|
66
75
|
receiver?.complete(false);
|
|
67
76
|
if (controller.signal.aborted) throw new CliError(expired
|
|
68
|
-
? "Sign-in expired; run `requestshield
|
|
77
|
+
? "Sign-in expired; run `requestshield login` again"
|
|
69
78
|
: "Sign-in was cancelled", {code: expired ? "SIGNIN_EXPIRED" : "SIGNIN_CANCELLED", exitCode: 3});
|
|
70
79
|
if (error instanceof CliError) throw error;
|
|
71
|
-
throw new CliError("Sign-in failed; run `requestshield
|
|
80
|
+
throw new CliError("Sign-in failed; run `requestshield login` again", {code: "SIGNIN_FAILED", exitCode: 3});
|
|
72
81
|
} finally {
|
|
73
82
|
clearTimeout(timer);
|
|
74
83
|
interruptSource.removeListener("SIGINT", cancel);
|
package/src/commands/signout.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { printData } from "./output.mjs";
|
|
4
|
+
|
|
5
|
+
/** @param {{json?: boolean}} options
|
|
4
6
|
* @param {{sessions: {signout(): Promise<{profile: import('../config.mjs').Profile, removed: boolean}>}, log: (message: string) => void}} deps
|
|
5
7
|
*/
|
|
6
|
-
export async function signout(
|
|
8
|
+
export async function signout(options, deps) {
|
|
7
9
|
const { profile, removed } = await deps.sessions.signout();
|
|
8
|
-
|
|
10
|
+
if (options.json) printData({profile, removed}, deps);
|
|
11
|
+
else deps.log(removed ? `Signed out locally from ${profile}.` : `No saved local session for ${profile}.`);
|
|
12
|
+
return {profile, removed};
|
|
9
13
|
}
|
|
@@ -2,46 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
import readline from "node:readline/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { stdin,
|
|
5
|
+
import { stdin, stderr } from "node:process";
|
|
6
6
|
import { CliError } from "../errors.mjs";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* @
|
|
11
|
+
* @typedef {{ currentVersion: string, packageName: string, profile?: "qat" | "stg" | "prod", json?: boolean, yes?: boolean, log: (message: string) => void, warn?: (message: string) => void, fetchImpl?: typeof fetch, confirm?: (latestVersion: string) => Promise<boolean>, install?: (packageName: string, version: string) => Promise<void> }} UpdateOptions
|
|
12
|
+
* @typedef {{ profile: "qat" | "stg" | "prod", currentVersion: string, latestVersion: string | null, updateAvailable: boolean | null, action: "check" | "apply", outcome: "source_checkout" | "up_to_date" | "available" | "installed" | "cancelled", target?: string }} UpdateResult
|
|
12
13
|
*/
|
|
14
|
+
|
|
15
|
+
/** @param {UpdateOptions} options */
|
|
13
16
|
export async function checkForUpdate(options) {
|
|
17
|
+
const result = await inspectUpdate(options, "check");
|
|
18
|
+
printResult(result, options);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @param {UpdateOptions} options */
|
|
23
|
+
export async function applyUpdate(options) {
|
|
24
|
+
const result = await inspectUpdate(options, "apply");
|
|
25
|
+
if (result.outcome !== "available" || result.latestVersion === null) {
|
|
26
|
+
printResult(result, options);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const warn = options.warn ?? ((message) => console.error(message));
|
|
31
|
+
warn(`Update target: global npm installation of ${options.packageName}.`);
|
|
32
|
+
if (options.json && !options.yes) {
|
|
33
|
+
throw confirmationRequired();
|
|
34
|
+
}
|
|
35
|
+
const accepted = options.yes || await (options.confirm ?? confirmUpdate)(result.latestVersion);
|
|
36
|
+
if (!accepted) {
|
|
37
|
+
result.outcome = "cancelled";
|
|
38
|
+
printResult(result, options);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
warn(`Installing RequestShield ${result.latestVersion} globally with npm...`);
|
|
43
|
+
await (options.install ?? installWithNpm)(options.packageName, result.latestVersion);
|
|
44
|
+
result.outcome = "installed";
|
|
45
|
+
printResult(result, options);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {UpdateOptions} options @param {"check" | "apply"} action @returns {Promise<UpdateResult>} */
|
|
50
|
+
async function inspectUpdate(options, action) {
|
|
14
51
|
const profile = options.profile ?? "prod";
|
|
15
52
|
if (profile !== "prod") {
|
|
16
|
-
|
|
17
|
-
options.log("Update the checkout with your normal Git workflow.");
|
|
18
|
-
options.log(`Then run from requestshield-cli/: npx --offline --prefix ./dev requestshield-${profile} <command>`);
|
|
19
|
-
return;
|
|
53
|
+
return { profile, currentVersion: options.currentVersion, latestVersion: null, updateAvailable: null, action, outcome: "source_checkout" };
|
|
20
54
|
}
|
|
21
|
-
const latestVersion = await fetchLatestVersion(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
const latestVersion = await fetchLatestVersion(options.packageName, options.fetchImpl ?? fetch);
|
|
56
|
+
const updateAvailable = compareVersions(latestVersion, options.currentVersion) > 0;
|
|
57
|
+
return {
|
|
58
|
+
profile,
|
|
59
|
+
currentVersion: options.currentVersion,
|
|
60
|
+
latestVersion,
|
|
61
|
+
updateAvailable,
|
|
62
|
+
action,
|
|
63
|
+
outcome: updateAvailable ? "available" : "up_to_date",
|
|
64
|
+
target: `global npm installation of ${options.packageName}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
28
67
|
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
/** @param {UpdateResult} result @param {UpdateOptions} options */
|
|
69
|
+
function printResult(result, options) {
|
|
70
|
+
if (options.json) {
|
|
71
|
+
options.log(JSON.stringify({ data: result }, null, 2));
|
|
31
72
|
return;
|
|
32
73
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
options.log("Update cancelled.");
|
|
74
|
+
if (result.outcome === "source_checkout") {
|
|
75
|
+
options.log(`requestshield-${result.profile} runs this local repository checkout and has no separately published npm release.`);
|
|
76
|
+
options.log("Update the checkout with your normal Git workflow.");
|
|
77
|
+
options.log(`Then run from requestshield-cli/: npx --offline --prefix ./dev requestshield-${result.profile} <command>`);
|
|
38
78
|
return;
|
|
39
79
|
}
|
|
40
|
-
|
|
41
|
-
options.log(`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
options.log(`Current version: ${result.currentVersion}`);
|
|
81
|
+
options.log(`Latest version: ${result.latestVersion}`);
|
|
82
|
+
if (result.outcome === "up_to_date") options.log("RequestShield is already up to date.");
|
|
83
|
+
if (result.outcome === "available") {
|
|
84
|
+
options.log("Run requestshield update apply to install the update.");
|
|
85
|
+
options.log(`Update target: ${result.target}.`);
|
|
86
|
+
}
|
|
87
|
+
if (result.outcome === "cancelled") options.log("Update cancelled.");
|
|
88
|
+
if (result.outcome === "installed") {
|
|
89
|
+
options.log(`RequestShield ${result.latestVersion} was installed successfully (global npm installation).`);
|
|
90
|
+
options.log("Open a new terminal before running RequestShield again.");
|
|
91
|
+
}
|
|
45
92
|
}
|
|
46
93
|
|
|
47
94
|
/** @param {string} packageName @param {typeof fetch} fetchImpl */
|
|
@@ -50,8 +97,8 @@ async function fetchLatestVersion(packageName, fetchImpl) {
|
|
|
50
97
|
let response;
|
|
51
98
|
try {
|
|
52
99
|
response = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
|
|
53
|
-
} catch
|
|
54
|
-
throw new CliError(
|
|
100
|
+
} catch {
|
|
101
|
+
throw new CliError("Could not check the latest RequestShield version. Check your connection and retry.", {
|
|
55
102
|
code: "UPDATE_CHECK_FAILED",
|
|
56
103
|
exitCode: 7,
|
|
57
104
|
});
|
|
@@ -64,7 +111,14 @@ async function fetchLatestVersion(packageName, fetchImpl) {
|
|
|
64
111
|
});
|
|
65
112
|
}
|
|
66
113
|
|
|
67
|
-
|
|
114
|
+
let body;
|
|
115
|
+
try {
|
|
116
|
+
body = await response.json();
|
|
117
|
+
} catch {
|
|
118
|
+
throw new CliError("The npm registry returned an invalid RequestShield version", {
|
|
119
|
+
code: "INVALID_UPDATE_RESPONSE",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
68
122
|
const version = body && typeof body === "object" ? Reflect.get(body, "version") : undefined;
|
|
69
123
|
if (typeof version !== "string" || !parseVersion(version)) {
|
|
70
124
|
throw new CliError("The npm registry returned an invalid RequestShield version", {
|
|
@@ -76,13 +130,8 @@ async function fetchLatestVersion(packageName, fetchImpl) {
|
|
|
76
130
|
|
|
77
131
|
/** @param {string} latestVersion */
|
|
78
132
|
async function confirmUpdate(latestVersion) {
|
|
79
|
-
if (!stdin.isTTY || !
|
|
80
|
-
|
|
81
|
-
code: "UPDATE_CONFIRMATION_REQUIRED",
|
|
82
|
-
exitCode: 2,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const prompt = readline.createInterface({ input: stdin, output: stdout });
|
|
133
|
+
if (!stdin.isTTY || !stderr.isTTY) throw confirmationRequired();
|
|
134
|
+
const prompt = readline.createInterface({ input: stdin, output: stderr });
|
|
86
135
|
try {
|
|
87
136
|
const answer = await prompt.question(`Install RequestShield ${latestVersion} globally with npm? (y/N): `);
|
|
88
137
|
return answer.trim().toLowerCase() === "y";
|
|
@@ -91,16 +140,26 @@ async function confirmUpdate(latestVersion) {
|
|
|
91
140
|
}
|
|
92
141
|
}
|
|
93
142
|
|
|
143
|
+
function confirmationRequired() {
|
|
144
|
+
return new CliError("Installing an update requires confirmation. Use an interactive terminal or pass --yes to requestshield update apply.", {
|
|
145
|
+
code: "UPDATE_CONFIRMATION_REQUIRED",
|
|
146
|
+
exitCode: 2,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
/** @param {string} packageName @param {string} version */
|
|
95
151
|
async function installWithNpm(packageName, version) {
|
|
96
152
|
await new Promise((resolve, reject) => {
|
|
97
153
|
const child = spawn("npm", ["install", "--global", `${packageName}@${version}`], {
|
|
98
154
|
shell: process.platform === "win32",
|
|
99
|
-
|
|
155
|
+
// npm's output is diagnostic. Keep command stdout available for JSON.
|
|
156
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
100
157
|
windowsHide: true,
|
|
101
158
|
});
|
|
102
|
-
child.
|
|
103
|
-
|
|
159
|
+
child.stdout?.pipe(stderr, { end: false });
|
|
160
|
+
child.stderr?.pipe(stderr, { end: false });
|
|
161
|
+
child.once("error", () => {
|
|
162
|
+
reject(new CliError("Could not start npm. Check that npm is installed and available.", {
|
|
104
163
|
code: "UPDATE_FAILED",
|
|
105
164
|
exitCode: 1,
|
|
106
165
|
}));
|
|
@@ -139,9 +198,4 @@ function parseVersion(value) {
|
|
|
139
198
|
};
|
|
140
199
|
}
|
|
141
200
|
|
|
142
|
-
/** @param {unknown} error */
|
|
143
|
-
function messageOf(error) {
|
|
144
|
-
return error instanceof Error ? error.message : String(error);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
201
|
export { compareVersions };
|