requestshield 0.1.5 → 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.
- package/README.md +414 -276
- package/config/.env.prod +7 -0
- package/package.json +8 -5
- package/skills/requestshield/SKILL.md +55 -63
- package/skills/requestshield/assets/AGENTS.codex.md +17 -17
- package/skills/requestshield/references/backend-java-core.md +3 -3
- package/skills/requestshield/references/backend-spring-boot.md +3 -3
- package/skills/requestshield/references/browser-manual.md +4 -4
- package/skills/requestshield/references/browser-seamless.md +7 -15
- package/skills/requestshield/references/cli.md +93 -169
- package/skills/requestshield/references/integration-planning.md +20 -47
- package/skills/requestshield/references/troubleshooting.md +26 -30
- package/src/api-client.mjs +106 -165
- package/src/args.mjs +108 -151
- package/src/browser-opener.mjs +32 -0
- package/src/cli.mjs +50 -28
- package/src/commands/agent-setup.mjs +34 -37
- package/src/commands/application-mutations.mjs +33 -0
- package/src/commands/application-response.mjs +55 -0
- package/src/commands/apps-get.mjs +3 -47
- package/src/commands/apps-list.mjs +40 -36
- package/src/commands/auth-status.mjs +37 -0
- package/src/commands/keys-create.mjs +7 -38
- package/src/commands/mutation-support.mjs +110 -0
- package/src/commands/secret-commands.mjs +45 -0
- package/src/commands/signin.mjs +70 -57
- package/src/commands/signout.mjs +9 -0
- package/src/commands/update-check.mjs +12 -4
- package/src/config.mjs +145 -3
- package/src/entrypoint.mjs +24 -0
- package/src/errors.mjs +3 -1
- package/src/main.mjs +2 -21
- package/src/oauth-client.mjs +153 -0
- package/src/oauth-loopback.mjs +120 -0
- package/src/session-files.mjs +213 -0
- package/src/session-store.mjs +177 -64
- package/src/commands/billing-get.mjs +0 -110
- package/src/commands/challenge-volume.mjs +0 -81
- package/src/commands/contract.mjs +0 -106
package/src/cli.mjs
CHANGED
|
@@ -4,26 +4,34 @@
|
|
|
4
4
|
import { parseArgs } from "./args.mjs";
|
|
5
5
|
import { ManagementApiClient } from "./api-client.mjs";
|
|
6
6
|
import { SessionStore } from "./session-store.mjs";
|
|
7
|
+
import { OAuthClient } from "./oauth-client.mjs";
|
|
7
8
|
|
|
8
9
|
import { signin } from "./commands/signin.mjs";
|
|
9
10
|
import { createKeys } from "./commands/keys-create.mjs";
|
|
10
|
-
import {
|
|
11
|
+
import { renameApp, setAppEnabled } from "./commands/application-mutations.mjs";
|
|
12
|
+
import { rotateSecret, revealSecret, revokeSecret } from "./commands/secret-commands.mjs";
|
|
13
|
+
import { showAuthStatus } from "./commands/auth-status.mjs";
|
|
14
|
+
import { signout } from "./commands/signout.mjs";
|
|
11
15
|
import { listApps } from "./commands/apps-list.mjs";
|
|
12
16
|
import { getApp } from "./commands/apps-get.mjs";
|
|
13
|
-
import { showChallengeVolume } from "./commands/challenge-volume.mjs";
|
|
14
|
-
import { showBilling } from "./commands/billing-get.mjs";
|
|
15
17
|
import { setupAgent } from "./commands/agent-setup.mjs";
|
|
16
18
|
import { checkForUpdate } from "./commands/update-check.mjs";
|
|
17
19
|
|
|
18
20
|
import packageJson from "../package.json" with { type: "json" };
|
|
19
|
-
import {
|
|
21
|
+
import { getApiUrl, getAuthorizationIssuer, getCommandInvocation, getCommandName, getOAuthConfig } from "./config.mjs";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* @typedef {{
|
|
23
25
|
* log: (message: string) => void,
|
|
26
|
+
* warn?: (message: string) => void,
|
|
24
27
|
* api?: ManagementApiClient,
|
|
25
28
|
* sessions?: SessionStore,
|
|
29
|
+
* oauth?: OAuthClient,
|
|
30
|
+
* authorizationIssuer?: string,
|
|
31
|
+
* openBrowser?: typeof import('./browser-opener.mjs').openBrowser,
|
|
32
|
+
* signal?: AbortSignal,
|
|
26
33
|
* env?: NodeJS.ProcessEnv,
|
|
34
|
+
* profile?: import('./config.mjs').Profile,
|
|
27
35
|
* homeDir?: string,
|
|
28
36
|
* sourceDir?: string,
|
|
29
37
|
* executablePath?: string,
|
|
@@ -68,19 +76,20 @@ const COMMAND_HANDLERS = {
|
|
|
68
76
|
"help": {
|
|
69
77
|
requiresApiSession: false,
|
|
70
78
|
handler: (parsed, context) =>
|
|
71
|
-
context.log(parsed.help),
|
|
79
|
+
context.log(parsed.help.replace(/^ requestshield /gm, ` ${getCommandInvocation(context.profile)} `)),
|
|
72
80
|
},
|
|
73
81
|
|
|
74
82
|
"version": {
|
|
75
83
|
requiresApiSession: false,
|
|
76
84
|
handler: (_parsed, context) =>
|
|
77
|
-
context.log(
|
|
85
|
+
context.log(`${getCommandName(context.profile)} ${packageJson.version}`),
|
|
78
86
|
},
|
|
79
87
|
|
|
80
88
|
"update-check": {
|
|
81
89
|
requiresApiSession: false,
|
|
82
90
|
handler: (_parsed, context) =>
|
|
83
91
|
checkForUpdate({
|
|
92
|
+
profile: context.profile,
|
|
84
93
|
currentVersion: packageJson.version,
|
|
85
94
|
packageName: packageJson.name,
|
|
86
95
|
log: context.log,
|
|
@@ -97,9 +106,15 @@ const COMMAND_HANDLERS = {
|
|
|
97
106
|
},
|
|
98
107
|
|
|
99
108
|
"signin": {
|
|
100
|
-
requiresApiSession:
|
|
101
|
-
handler: (
|
|
102
|
-
|
|
109
|
+
requiresApiSession: false,
|
|
110
|
+
handler: (parsed, context) => {
|
|
111
|
+
const env = context.env ?? process.env;
|
|
112
|
+
const oauth = context.oauth ?? new OAuthClient({config: getOAuthConfig(context.profile), fetchImpl: context.fetchImpl});
|
|
113
|
+
const authorizationIssuer = context.authorizationIssuer ?? (context.oauth
|
|
114
|
+
? oauth.config.issuer : getAuthorizationIssuer(context.profile, oauth.config.issuer));
|
|
115
|
+
const sessions = context.sessions ?? new SessionStore({env, homeDir: context.homeDir, oauth, config: oauth.config, profile: context.profile});
|
|
116
|
+
return signin(parsed, {...context, oauth, sessions, authorizationIssuer});
|
|
117
|
+
},
|
|
103
118
|
},
|
|
104
119
|
|
|
105
120
|
"keys-create": {
|
|
@@ -108,11 +123,14 @@ const COMMAND_HANDLERS = {
|
|
|
108
123
|
createKeys(parsed, requireApiSession(context)),
|
|
109
124
|
},
|
|
110
125
|
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
126
|
+
"keys-rotate": {requiresApiSession: true, handler: (parsed, context) => rotateSecret(parsed, requireApiSession(context))},
|
|
127
|
+
"keys-reveal": {requiresApiSession: true, handler: (parsed, context) => revealSecret(parsed, requireApiSession(context))},
|
|
128
|
+
"keys-revoke": {requiresApiSession: true, handler: (parsed, context) => revokeSecret(parsed, requireApiSession(context))},
|
|
129
|
+
"apps-rename": {requiresApiSession: true, handler: (parsed, context) => renameApp(parsed, requireApiSession(context))},
|
|
130
|
+
"apps-enable": {requiresApiSession: true, handler: (parsed, context) => setAppEnabled(parsed, requireApiSession(context))},
|
|
131
|
+
"apps-disable": {requiresApiSession: true, handler: (parsed, context) => setAppEnabled(parsed, requireApiSession(context))},
|
|
132
|
+
"auth-status": {requiresApiSession: false, handler: (parsed, context) => showAuthStatus(parsed, {...context, sessions: localSessions(context)})},
|
|
133
|
+
"signout": {requiresApiSession: false, handler: (parsed, context) => signout(parsed, {...context, sessions: localSessions(context)})},
|
|
116
134
|
|
|
117
135
|
"apps-list": {
|
|
118
136
|
requiresApiSession: true,
|
|
@@ -126,19 +144,13 @@ const COMMAND_HANDLERS = {
|
|
|
126
144
|
getApp(parsed, requireApiSession(context)),
|
|
127
145
|
},
|
|
128
146
|
|
|
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
147
|
};
|
|
141
148
|
|
|
149
|
+
/** @param {CommandContext} context */
|
|
150
|
+
function localSessions(context) {
|
|
151
|
+
return context.sessions ?? new SessionStore({env: context.env, homeDir: context.homeDir, profile: context.profile});
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
/**
|
|
143
155
|
* Narrow the shared context before an authenticated handler can use it.
|
|
144
156
|
*
|
|
@@ -161,9 +173,15 @@ function requireApiSession(context) {
|
|
|
161
173
|
* @param {string[]} argv
|
|
162
174
|
* @param {{
|
|
163
175
|
* log?: (message: string) => void,
|
|
176
|
+
* warn?: (message: string) => void,
|
|
164
177
|
* api?: ManagementApiClient,
|
|
165
178
|
* sessions?: SessionStore,
|
|
179
|
+
* oauth?: OAuthClient,
|
|
180
|
+
* authorizationIssuer?: string,
|
|
181
|
+
* openBrowser?: typeof import('./browser-opener.mjs').openBrowser,
|
|
182
|
+
* signal?: AbortSignal,
|
|
166
183
|
* env?: NodeJS.ProcessEnv,
|
|
184
|
+
* profile?: import('./config.mjs').Profile,
|
|
167
185
|
* homeDir?: string,
|
|
168
186
|
* sourceDir?: string,
|
|
169
187
|
* executablePath?: string,
|
|
@@ -182,6 +200,8 @@ function requireApiSession(context) {
|
|
|
182
200
|
* }} [deps]
|
|
183
201
|
*/
|
|
184
202
|
export async function run(argv, deps = {}) {
|
|
203
|
+
const profile = deps.profile ?? "prod";
|
|
204
|
+
getCommandName(profile);
|
|
185
205
|
const parsed = parseArgs(argv);
|
|
186
206
|
|
|
187
207
|
const log =
|
|
@@ -205,7 +225,9 @@ export async function run(argv, deps = {}) {
|
|
|
205
225
|
/** @type {CommandContext} */
|
|
206
226
|
const context = {
|
|
207
227
|
...deps,
|
|
228
|
+
profile,
|
|
208
229
|
log,
|
|
230
|
+
warn: deps.warn ?? ((message) => console.error(message)),
|
|
209
231
|
};
|
|
210
232
|
|
|
211
233
|
/*
|
|
@@ -220,9 +242,8 @@ export async function run(argv, deps = {}) {
|
|
|
220
242
|
const api =
|
|
221
243
|
deps.api ??
|
|
222
244
|
new ManagementApiClient({
|
|
223
|
-
baseUrl:
|
|
224
|
-
|
|
225
|
-
DEFAULT_MANAGEMENT_API_URL,
|
|
245
|
+
baseUrl: getApiUrl(profile),
|
|
246
|
+
fetchImpl: deps.fetchImpl,
|
|
226
247
|
});
|
|
227
248
|
|
|
228
249
|
const sessions =
|
|
@@ -230,6 +251,7 @@ export async function run(argv, deps = {}) {
|
|
|
230
251
|
new SessionStore({
|
|
231
252
|
env,
|
|
232
253
|
homeDir: deps.homeDir,
|
|
254
|
+
profile,
|
|
233
255
|
});
|
|
234
256
|
|
|
235
257
|
const authenticatedContext = {
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { cp, lstat, mkdir, realpath, rename, rm
|
|
3
|
+
import { cp, lstat, mkdir, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { CliError } from "../errors.mjs";
|
|
9
10
|
import { detectAgents } from "../agent-detector.mjs";
|
|
10
|
-
import { getAsset, isSea } from "node:sea";
|
|
11
|
-
|
|
12
|
-
const skillAsset = "requestshield-skill.md";
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
@@ -39,39 +37,11 @@ export async function setupAgent(options, deps) {
|
|
|
39
37
|
const staging = path.join(skillsRoot, `.requestshield-${randomUUID()}.tmp`);
|
|
40
38
|
const backup = `${destination}.${randomUUID()}.backup`;
|
|
41
39
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
} else if (isSea()) {
|
|
49
|
-
// A single-executable build reads the skill from its embedded assets.
|
|
50
|
-
await mkdir(staging);
|
|
51
|
-
|
|
52
|
-
await writeFile(
|
|
53
|
-
path.join(staging, "SKILL.md"),
|
|
54
|
-
Buffer.from(getAsset(skillAsset)),
|
|
55
|
-
);
|
|
56
|
-
} else {
|
|
57
|
-
// Source, npm, and on-disk CJS builds keep the skill beside the program.
|
|
58
|
-
//
|
|
59
|
-
// src/main.mjs -> ../skills/requestshield
|
|
60
|
-
// dist/requestshield.cjs -> ../skills/requestshield
|
|
61
|
-
// npm link and global npm installs expose a symlink in their bin folder.
|
|
62
|
-
// Follow it before resolving the adjacent packaged skills directory.
|
|
63
|
-
const executable = await realpath(deps.executablePath ?? process.argv[1]);
|
|
64
|
-
const executableDir = path.dirname(executable);
|
|
65
|
-
const source = path.resolve(
|
|
66
|
-
executableDir,
|
|
67
|
-
"../skills/requestshield",
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
await cp(source, staging, {
|
|
71
|
-
recursive: true,
|
|
72
|
-
errorOnExist: true,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
40
|
+
const source = deps.sourceDir ?? await resolveSkillSource(deps.executablePath);
|
|
41
|
+
await cp(source, staging, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
errorOnExist: true,
|
|
44
|
+
});
|
|
75
45
|
|
|
76
46
|
// Keep the old installation recoverable until the staged copy is in place.
|
|
77
47
|
if (exists) await rename(destination, backup);
|
|
@@ -90,6 +60,33 @@ export async function setupAgent(options, deps) {
|
|
|
90
60
|
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
91
61
|
}
|
|
92
62
|
|
|
63
|
+
/** @param {string} [executablePath] */
|
|
64
|
+
async function resolveSkillSource(executablePath) {
|
|
65
|
+
// Resolve from this module, not the user's cwd or the shell's argv. Following
|
|
66
|
+
// the executable also supports explicitly supplied npm-link test fixtures.
|
|
67
|
+
const executable = await realpath(
|
|
68
|
+
executablePath ?? fileURLToPath(new URL("../main.mjs", import.meta.url)),
|
|
69
|
+
);
|
|
70
|
+
const packageRoot = path.resolve(path.dirname(executable), "..");
|
|
71
|
+
const bundled = path.join(packageRoot, "skills", "requestshield");
|
|
72
|
+
if (await pathExists(path.join(bundled, "SKILL.md"))) return bundled;
|
|
73
|
+
|
|
74
|
+
// Only a source checkout has both the private runner manifest and packaging
|
|
75
|
+
// script. Installed packages must never pick up an unrelated sibling skill.
|
|
76
|
+
const devManifestPath = path.join(packageRoot, "dev", "package.json");
|
|
77
|
+
if (await pathExists(devManifestPath)
|
|
78
|
+
&& await pathExists(path.join(packageRoot, "scripts", "package-skill.mjs"))) {
|
|
79
|
+
const devManifest = JSON.parse(await readFile(devManifestPath, "utf8"));
|
|
80
|
+
const canonical = path.resolve(packageRoot, "../skills/requestshield");
|
|
81
|
+
if (devManifest.private === true
|
|
82
|
+
&& devManifest.name === "@intellifend/requestshield-dev"
|
|
83
|
+
&& await pathExists(path.join(canonical, "SKILL.md"))) return canonical;
|
|
84
|
+
}
|
|
85
|
+
throw new CliError("The RequestShield skill is missing. Restore this source checkout or reinstall the published package.", {
|
|
86
|
+
code: "SKILL_SOURCE_MISSING",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
93
90
|
/**
|
|
94
91
|
* Explicit selection bypasses detection. Automatic setup selects a single
|
|
95
92
|
* detected agent or asks the user when both agents are present.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { parseApplicationResponse } from "./application-response.mjs";
|
|
3
|
+
import { confirmAction, mutationKey, parseAccepted, printAccepted, validateHttpStatus, withMutationRecovery } from "./mutation-support.mjs";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./mutation-support.mjs').MutationResponse} MutationResponse */
|
|
6
|
+
/** @typedef {import('./mutation-support.mjs').CommandOutput & {sessions: {loadToken(): Promise<string>}}} Dependencies */
|
|
7
|
+
|
|
8
|
+
/** @param {{appKey: string, name: string, yes?: boolean, idempotencyKey?: string}} options
|
|
9
|
+
* @param {Dependencies & {api: {renameApp(token: string, appKey: string, options: {name: string, idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
10
|
+
*/
|
|
11
|
+
export async function renameApp(options, deps) {
|
|
12
|
+
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
13
|
+
const token = await deps.sessions.loadToken();
|
|
14
|
+
const application = await withMutationRecovery(idempotencyKey, deps, async () => {
|
|
15
|
+
const result = await deps.api.renameApp(token, options.appKey, {name: options.name, idempotencyKey});
|
|
16
|
+
validateHttpStatus(result, 200);
|
|
17
|
+
return parseApplicationResponse(result.body, options.appKey);
|
|
18
|
+
});
|
|
19
|
+
deps.log(JSON.stringify({data: application}, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @param {{appKey: string, enabled: boolean, yes?: boolean, idempotencyKey?: string}} options
|
|
23
|
+
* @param {Dependencies & {api: {setAppEnabled(token: string, appKey: string, options: {enabled: boolean, idempotencyKey: string}): Promise<MutationResponse>}}} deps
|
|
24
|
+
*/
|
|
25
|
+
export async function setAppEnabled(options, deps) {
|
|
26
|
+
if (!options.enabled) await confirmAction(options, deps, `Disable application ${options.appKey}?`, "DISABLE");
|
|
27
|
+
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
28
|
+
const token = await deps.sessions.loadToken();
|
|
29
|
+
await withMutationRecovery(idempotencyKey, deps, async () => {
|
|
30
|
+
parseAccepted(await deps.api.setAppEnabled(token, options.appKey, {enabled: options.enabled, idempotencyKey}));
|
|
31
|
+
});
|
|
32
|
+
printAccepted(options.enabled ? "Application enable" : "Application disable", options.appKey, deps);
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { CliError } from "../errors.mjs";
|
|
3
|
+
|
|
4
|
+
export const APPLICATION_STATUSES = new Set(["attention_required", "pending", "disabled", "revoked", "enabled"]);
|
|
5
|
+
|
|
6
|
+
/** @typedef {{appKey: string, name: string, status: string, createdAt: string, updatedAt: string}} Application */
|
|
7
|
+
|
|
8
|
+
/** Construct metadata from the implemented API schema; never relay extra fields.
|
|
9
|
+
* @param {unknown} value @param {string} [requestedAppKey] @returns {Application}
|
|
10
|
+
*/
|
|
11
|
+
export function parseApplication(value, requestedAppKey) {
|
|
12
|
+
const data = responseObject(value);
|
|
13
|
+
const {appKey, name, status, createdAt, updatedAt} = data;
|
|
14
|
+
if (typeof appKey !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/.test(appKey)) {
|
|
15
|
+
throw invalidResponse("The application response contained an invalid App Key");
|
|
16
|
+
}
|
|
17
|
+
if (requestedAppKey !== undefined && appKey !== requestedAppKey) {
|
|
18
|
+
throw invalidResponse("The application response did not match the requested App Key");
|
|
19
|
+
}
|
|
20
|
+
if (typeof name !== "string" || !name.trim() || [...name].length > 100 || /[\p{Cc}\p{Cs}]/u.test(name)) {
|
|
21
|
+
throw invalidResponse("The application response contained an invalid name");
|
|
22
|
+
}
|
|
23
|
+
if (typeof status !== "string" || !APPLICATION_STATUSES.has(status)) {
|
|
24
|
+
throw invalidResponse("The application response contained an unsupported status");
|
|
25
|
+
}
|
|
26
|
+
if (!isTimestamp(createdAt) || !isTimestamp(updatedAt)) {
|
|
27
|
+
throw invalidResponse("The application response contained invalid timestamps");
|
|
28
|
+
}
|
|
29
|
+
return {appKey, name, status, createdAt, updatedAt};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {unknown} body @param {string} requestedAppKey */
|
|
33
|
+
export function parseApplicationResponse(body, requestedAppKey) {
|
|
34
|
+
return parseApplication(responseObject(body).data, requestedAppKey);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {unknown} value @returns {Record<string, unknown>} */
|
|
38
|
+
export function responseObject(value) {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
40
|
+
throw invalidResponse("The API response did not contain the expected object");
|
|
41
|
+
}
|
|
42
|
+
return /** @type {Record<string, unknown>} */ (value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {unknown} value @returns {value is string} */
|
|
46
|
+
function isTimestamp(value) {
|
|
47
|
+
return typeof value === "string"
|
|
48
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
49
|
+
&& Number.isFinite(Date.parse(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {string} message */
|
|
53
|
+
export function invalidResponse(message) {
|
|
54
|
+
return new CliError(message, {code: "INVALID_RESPONSE"});
|
|
55
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { objectString } from "../api-client.mjs";
|
|
5
|
-
|
|
6
|
-
const APPLICATION_STATUSES = new Set(["ready", "active", "deactivated"]);
|
|
3
|
+
import { parseApplicationResponse } from "./application-response.mjs";
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* @param {{ appKey: string }} options
|
|
@@ -17,48 +14,7 @@ export async function getApp(options, deps) {
|
|
|
17
14
|
await deps.api.getApp(accessToken, options.appKey);
|
|
18
15
|
|
|
19
16
|
const app =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
deps.log(JSON.stringify({ ok: true, data: app }, null, 2));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** @param {unknown} body @param {string} requestedAppKey */
|
|
26
|
-
function parseApp(body, requestedAppKey) {
|
|
27
|
-
const ok = body && typeof body === "object" ?
|
|
28
|
-
Reflect.get(body, "ok") : undefined;
|
|
29
|
-
|
|
30
|
-
const data = body && typeof body === "object" ?
|
|
31
|
-
Reflect.get(body, "data") : undefined;
|
|
17
|
+
parseApplicationResponse(result.body, options.appKey);
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
throw invalidResponse(
|
|
35
|
-
"The application response did not contain ok=true and data"
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const appKey = objectString(data, "app_key");
|
|
40
|
-
const appName = objectString(data, "name");
|
|
41
|
-
const status = objectString(data, "status");
|
|
42
|
-
|
|
43
|
-
if (appKey !== requestedAppKey) {
|
|
44
|
-
throw invalidResponse("The application response did not match the requested App Key");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!appName) {
|
|
48
|
-
throw invalidResponse("The application response did not contain name");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!status || !APPLICATION_STATUSES.has(status)) {
|
|
52
|
-
throw invalidResponse(
|
|
53
|
-
"The application response status must be ready, active, or deactivated",
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Construct a new object so unexpected response fields, especially secrets, cannot be printed.
|
|
58
|
-
return { app_key: appKey, name: appName, status };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** @param {string} message */
|
|
62
|
-
function invalidResponse(message) {
|
|
63
|
-
return new CliError(message, { code: "INVALID_RESPONSE" });
|
|
19
|
+
deps.log(JSON.stringify({ data: app }, null, 2));
|
|
64
20
|
}
|
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { CliError } from "../errors.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { getCommandInvocation } from "../config.mjs";
|
|
5
|
+
import { invalidResponse, parseApplication, responseObject } from "./application-response.mjs";
|
|
5
6
|
|
|
6
7
|
const HEADERS = ["APP KEY", "NAME", "STATUS"];
|
|
8
|
+
const MAX_PAGES = 100;
|
|
7
9
|
|
|
8
10
|
/** @typedef {{ appKey: string, name: string, status: string, createdAt: string, updatedAt: string }} App */
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* @param {{
|
|
12
|
-
* @param {{
|
|
13
|
+
* @param {{json: boolean, limit?: number, cursor?: string, all?: boolean}} options
|
|
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
|
|
13
15
|
*/
|
|
14
16
|
export async function listApps(options, deps) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
/** @type {App[]} */
|
|
18
|
+
const apps = [];
|
|
19
|
+
let cursor = options.cursor;
|
|
20
|
+
let nextCursor = null;
|
|
21
|
+
const seen = new Set(cursor ? [cursor] : []);
|
|
22
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
23
|
+
// Long traversals can cross token expiry; each page may refresh safely.
|
|
24
|
+
const accessToken = await deps.sessions.loadToken();
|
|
25
|
+
const result = await deps.api.listApps(accessToken, {
|
|
26
|
+
...(options.limit !== undefined ? {limit: options.limit} : {}),
|
|
27
|
+
...(cursor !== undefined ? {cursor} : {}),
|
|
28
|
+
});
|
|
29
|
+
const parsed = parseApps(result.body);
|
|
30
|
+
if (parsed.data.length > (options.limit ?? 50)) throw invalidResponse("The applications response exceeded the requested page size");
|
|
31
|
+
apps.push(...parsed.data);
|
|
32
|
+
nextCursor = parsed.nextCursor;
|
|
33
|
+
if (nextCursor !== null && seen.has(nextCursor)) {
|
|
34
|
+
throw new CliError("The API repeated an application cursor; pagination stopped without returning partial results", {code: "PAGINATION_STALLED"});
|
|
35
|
+
}
|
|
36
|
+
if (!options.all || nextCursor === null) break;
|
|
37
|
+
seen.add(nextCursor);
|
|
38
|
+
cursor = nextCursor;
|
|
39
|
+
if (page === MAX_PAGES - 1) {
|
|
40
|
+
throw new CliError("Application pagination exceeded 100 pages; use --limit and --cursor to retrieve individual pages", {code: "PAGINATION_LIMIT"});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
22
43
|
|
|
23
44
|
if (options.json) {
|
|
24
45
|
deps.log(JSON.stringify({ data: apps, nextCursor }, null, 2));
|
|
@@ -26,20 +47,18 @@ export async function listApps(options, deps) {
|
|
|
26
47
|
}
|
|
27
48
|
|
|
28
49
|
if (apps.length === 0) {
|
|
29
|
-
deps.log(
|
|
30
|
-
|
|
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);
|
|
31
55
|
}
|
|
32
|
-
|
|
33
|
-
for (const line of formatTable(apps)) deps.log(line);
|
|
56
|
+
if (nextCursor !== null) deps.log(`More applications are available. Continue with \`${getCommandInvocation(deps.profile)} apps list --cursor ${nextCursor}\` or use --all.`);
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
/** @param {unknown} body @returns {{ data: App[], nextCursor: string | null }} */
|
|
37
60
|
function parseApps(body) {
|
|
38
|
-
const data = body
|
|
39
|
-
Reflect.get(body, "data") : undefined;
|
|
40
|
-
|
|
41
|
-
const nextCursor = body && typeof body === "object" ?
|
|
42
|
-
Reflect.get(body, "nextCursor") : undefined;
|
|
61
|
+
const {data, nextCursor} = responseObject(body);
|
|
43
62
|
|
|
44
63
|
if (!Array.isArray(data)) {
|
|
45
64
|
throw new CliError("The applications response did not contain a data array", {
|
|
@@ -47,29 +66,14 @@ function parseApps(body) {
|
|
|
47
66
|
});
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
if (nextCursor !== null && (typeof nextCursor !== "string" || nextCursor
|
|
69
|
+
if (nextCursor !== null && (typeof nextCursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(nextCursor))) {
|
|
51
70
|
throw new CliError("The applications response contained an invalid nextCursor", {
|
|
52
71
|
code: "INVALID_RESPONSE",
|
|
53
72
|
});
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
// Rebuild every item from approved metadata so unexpected API fields never reach output.
|
|
57
|
-
const apps = data.map(
|
|
58
|
-
const appKey = objectString(entry, "appKey");
|
|
59
|
-
const name = objectString(entry, "name");
|
|
60
|
-
const status = objectString(entry, "status");
|
|
61
|
-
const createdAt = objectString(entry, "createdAt");
|
|
62
|
-
const updatedAt = objectString(entry, "updatedAt");
|
|
63
|
-
|
|
64
|
-
if (!appKey || !name || !status || !createdAt || !updatedAt) {
|
|
65
|
-
throw new CliError(
|
|
66
|
-
`Application at position ${index + 1} is missing appKey, name, status, createdAt, or updatedAt`,
|
|
67
|
-
{ code: "INVALID_RESPONSE" },
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { appKey, name, status, createdAt, updatedAt };
|
|
72
|
-
});
|
|
76
|
+
const apps = data.map(entry => parseApplication(entry));
|
|
73
77
|
|
|
74
78
|
return { data: apps, nextCursor };
|
|
75
79
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @param {{json?: boolean}} options
|
|
4
|
+
* @param {{sessions: {status(): Promise<import('../session-store.mjs').SessionStatus>}, log: (message: string) => void}} deps
|
|
5
|
+
*/
|
|
6
|
+
export async function showAuthStatus({ json = false }, deps) {
|
|
7
|
+
const status = await deps.sessions.status();
|
|
8
|
+
// Rebuild the output so future storage fields can never expose credentials.
|
|
9
|
+
const result = {
|
|
10
|
+
profile: status.profile, apiUrl: status.apiUrl, issuer: status.issuer,
|
|
11
|
+
clientId: status.clientId, state: status.state, localOnly: true,
|
|
12
|
+
...(["valid", "expired", "refresh_uncertain"].includes(status.state) ? {
|
|
13
|
+
expiresAt: status.expiresAt, scopes: status.scopes ? [...status.scopes] : [],
|
|
14
|
+
} : {}),
|
|
15
|
+
};
|
|
16
|
+
if (json) {
|
|
17
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const descriptions = {
|
|
21
|
+
signed_out: "signed out",
|
|
22
|
+
valid: "locally valid",
|
|
23
|
+
expired: "access token expired; the next authenticated command will attempt refresh",
|
|
24
|
+
refresh_uncertain: "refresh outcome uncertain; sign in again",
|
|
25
|
+
config_mismatch: "saved session belongs to different configuration; sign in again",
|
|
26
|
+
invalid: "saved session is invalid; sign in again",
|
|
27
|
+
configuration_error: "selected profile configuration is incomplete or invalid",
|
|
28
|
+
};
|
|
29
|
+
deps.log(`Profile: ${result.profile}`);
|
|
30
|
+
deps.log(`Session (local): ${descriptions[result.state]}`);
|
|
31
|
+
deps.log(`API: ${result.apiUrl ?? "not configured"}`);
|
|
32
|
+
deps.log(`OAuth issuer: ${result.issuer ?? "not configured"}`);
|
|
33
|
+
deps.log(`OAuth client: ${result.clientId ?? "not configured"}`);
|
|
34
|
+
if (result.expiresAt !== undefined) deps.log(`Access token expires at: ${new Date(result.expiresAt).toUTCString()}`);
|
|
35
|
+
if (result.scopes) deps.log(`Granted scopes: ${result.scopes.join(" ")}`);
|
|
36
|
+
deps.log("This is local status; provider validity was not checked.");
|
|
37
|
+
}
|
|
@@ -1,46 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { stdin, stdout } from "node:process";
|
|
5
|
-
import { CliError } from "../errors.mjs";
|
|
6
|
-
import { objectString } from "../api-client.mjs";
|
|
3
|
+
import { mutationKey, parseIssuance, printIssuance, withMutationRecovery } from "./mutation-support.mjs";
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
|
-
* @param {{ yes
|
|
10
|
-
* @param {{
|
|
6
|
+
* @param {{appName: string, yes?: boolean, idempotencyKey?: string}} options
|
|
7
|
+
* @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
|
|
11
8
|
*/
|
|
12
9
|
export async function createKeys(options, deps) {
|
|
13
|
-
|
|
14
|
-
const accepted = await (deps.confirm ?? confirmRotation)();
|
|
15
|
-
if (!accepted) throw new CliError("Key rotation cancelled", { code: "CANCELLED", exitCode: 2 });
|
|
16
|
-
}
|
|
10
|
+
const idempotencyKey = mutationKey(options.idempotencyKey);
|
|
17
11
|
const token = await deps.sessions.loadToken();
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!appKey || !apiSecret) {
|
|
22
|
-
throw new CliError("The key rotation response did not contain appKey and apiSecret", {
|
|
23
|
-
code: "INVALID_RESPONSE",
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
deps.log("New RequestShield keys are active. Previous keys are deactivated.");
|
|
27
|
-
deps.log(`App Key: ${appKey}`);
|
|
28
|
-
deps.log(`Secret Key (shown once): ${apiSecret}`);
|
|
29
|
-
deps.log("Store the Secret Key in your backend secret manager now. The CLI did not save it.");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function confirmRotation() {
|
|
33
|
-
if (!stdin.isTTY || !stdout.isTTY) {
|
|
34
|
-
throw new CliError("Refusing non-interactive key rotation without --yes", {
|
|
35
|
-
code: "CONFIRMATION_REQUIRED",
|
|
36
|
-
exitCode: 2,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const prompt = readline.createInterface({ input: stdin, output: stdout });
|
|
40
|
-
try {
|
|
41
|
-
const answer = await prompt.question("This deactivates the current keys. Type ROTATE to continue: ");
|
|
42
|
-
return answer === "ROTATE";
|
|
43
|
-
} finally {
|
|
44
|
-
prompt.close();
|
|
45
|
-
}
|
|
12
|
+
const issuance = await withMutationRecovery(idempotencyKey, deps, async () =>
|
|
13
|
+
parseIssuance(await deps.api.createApp(token, {name: options.appName, idempotencyKey}), 201));
|
|
14
|
+
printIssuance(issuance, deps);
|
|
46
15
|
}
|