requestshield 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +421 -85
- package/config/.env.prod +7 -0
- package/package.json +21 -12
- package/skills/requestshield/SKILL.md +299 -307
- package/skills/requestshield/assets/AGENTS.codex.md +62 -62
- package/skills/requestshield/references/backend-java-core.md +128 -128
- package/skills/requestshield/references/backend-spring-boot.md +145 -145
- package/skills/requestshield/references/browser-manual.md +210 -210
- package/skills/requestshield/references/browser-seamless.md +156 -164
- package/skills/requestshield/references/cli.md +107 -182
- package/skills/requestshield/references/integration-planning.md +362 -389
- package/skills/requestshield/references/troubleshooting.md +114 -118
- package/src/agent-detector.mjs +102 -74
- package/src/api-client.mjs +115 -79
- package/src/args.mjs +140 -80
- package/src/browser-opener.mjs +32 -0
- package/src/cli.mjs +277 -51
- package/src/commands/agent-setup.mjs +182 -185
- package/src/commands/application-mutations.mjs +33 -0
- package/src/commands/application-response.mjs +55 -0
- package/src/commands/apps-get.mjs +20 -0
- package/src/commands/apps-list.mjs +94 -0
- 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 +150 -0
- package/src/entrypoint.mjs +24 -0
- package/src/errors.mjs +3 -1
- package/src/main.mjs +5 -24
- 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
|
@@ -1,185 +1,182 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import { cp, lstat, mkdir, realpath, rename, rm
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { createInterface } from "node:readline/promises";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
* detected
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
"",
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
"",
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { cp, lstat, mkdir, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { CliError } from "../errors.mjs";
|
|
10
|
+
import { detectAgents } from "../agent-detector.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ agent?: "codex" | "claude", force: boolean }} options
|
|
14
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, log: (message: string) => void, sourceDir?: string, executablePath?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
15
|
+
*/
|
|
16
|
+
export async function setupAgent(options, deps) {
|
|
17
|
+
const agent = await resolveAgent(options.agent, deps);
|
|
18
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
19
|
+
|
|
20
|
+
// Codex and Claude discover personal skills from different directories.
|
|
21
|
+
const skillsRoot = agent === "codex"
|
|
22
|
+
? path.join(homeDir, ".agents", "skills")
|
|
23
|
+
: path.join(homeDir, ".claude", "skills");
|
|
24
|
+
const destination = path.join(skillsRoot, "requestshield");
|
|
25
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const exists = await pathExists(destination);
|
|
28
|
+
if (exists && !options.force) {
|
|
29
|
+
throw new CliError(`RequestShield skill already exists at ${destination}; use --force to replace it`, {
|
|
30
|
+
code: "SKILL_EXISTS",
|
|
31
|
+
exitCode: 2,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Copy into a staging directory so an interrupted copy cannot leave a
|
|
36
|
+
// partially installed skill at the final destination.
|
|
37
|
+
const staging = path.join(skillsRoot, `.requestshield-${randomUUID()}.tmp`);
|
|
38
|
+
const backup = `${destination}.${randomUUID()}.backup`;
|
|
39
|
+
try {
|
|
40
|
+
const source = deps.sourceDir ?? await resolveSkillSource(deps.executablePath);
|
|
41
|
+
await cp(source, staging, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
errorOnExist: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Keep the old installation recoverable until the staged copy is in place.
|
|
47
|
+
if (exists) await rename(destination, backup);
|
|
48
|
+
await rename(staging, destination);
|
|
49
|
+
if (exists) await rm(backup, { recursive: true, force: true });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await rm(staging, { recursive: true, force: true });
|
|
52
|
+
if (await pathExists(backup)) {
|
|
53
|
+
await rm(destination, { recursive: true, force: true });
|
|
54
|
+
await rename(backup, destination);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const agentLabel = agent === "codex" ? "Codex" : "Claude";
|
|
59
|
+
deps.log(`Installed RequestShield skill for ${agentLabel} at ${destination}`);
|
|
60
|
+
deps.log(`Restart ${agentLabel} or start a new task before using the skill.`);
|
|
61
|
+
}
|
|
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
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Explicit selection bypasses detection. Automatic setup selects a single
|
|
92
|
+
* detected agent or asks the user when both agents are present.
|
|
93
|
+
*
|
|
94
|
+
* @param {"codex" | "claude" | undefined} requested
|
|
95
|
+
* @param {{ env?: NodeJS.ProcessEnv, homeDir?: string, detectAgents?: () => Promise<Array<"codex" | "claude">>, selectAgent?: (detected: Array<"codex" | "claude">) => Promise<"codex" | "claude"> }} deps
|
|
96
|
+
* @returns {Promise<"codex" | "claude">}
|
|
97
|
+
*/
|
|
98
|
+
async function resolveAgent(requested, deps) {
|
|
99
|
+
// A command-line choice is authoritative and avoids probing the machine.
|
|
100
|
+
if (requested) return requested;
|
|
101
|
+
|
|
102
|
+
const detected = deps.detectAgents
|
|
103
|
+
? await deps.detectAgents()
|
|
104
|
+
: await detectAgents({ env: deps.env, homeDir: deps.homeDir });
|
|
105
|
+
|
|
106
|
+
if (detected.length === 1) return detected[0];
|
|
107
|
+
if (detected.length === 0) {
|
|
108
|
+
throw new CliError(
|
|
109
|
+
[
|
|
110
|
+
"No supported coding agent detected.",
|
|
111
|
+
"",
|
|
112
|
+
"Choose one explicitly:",
|
|
113
|
+
" requestshield agent setup --codex",
|
|
114
|
+
" requestshield agent setup --claude",
|
|
115
|
+
].join("\n"),
|
|
116
|
+
{ code: "AGENT_NOT_FOUND", exitCode: 2 },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Never choose silently when both agents are present. Interactive users get
|
|
121
|
+
// a prompt; non-interactive callers receive a stable conflict error.
|
|
122
|
+
const selectAgent = deps.selectAgent ?? promptForAgent;
|
|
123
|
+
const selected = await selectAgent(detected);
|
|
124
|
+
if (!detected.includes(selected)) {
|
|
125
|
+
throw new CliError(`Selected agent is not available: ${selected}`, {
|
|
126
|
+
code: "INVALID_AGENT",
|
|
127
|
+
exitCode: 2,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return selected;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ask an interactive user which detected agent should receive the skill.
|
|
135
|
+
*
|
|
136
|
+
* @returns {Promise<"codex" | "claude">}
|
|
137
|
+
*/
|
|
138
|
+
async function promptForAgent() {
|
|
139
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
140
|
+
throw new CliError(
|
|
141
|
+
[
|
|
142
|
+
"Multiple coding agents detected.",
|
|
143
|
+
"",
|
|
144
|
+
"Choose one explicitly:",
|
|
145
|
+
" requestshield agent setup --codex",
|
|
146
|
+
" requestshield agent setup --claude",
|
|
147
|
+
].join("\n"),
|
|
148
|
+
{ code: "INSTALL_CONFLICT", exitCode: 2 },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const prompt = createInterface({
|
|
153
|
+
input: process.stdin,
|
|
154
|
+
output: process.stdout,
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
process.stdout.write(
|
|
158
|
+
"Multiple coding agents detected.\n\n 1. Codex\n 2. Claude\n\n",
|
|
159
|
+
);
|
|
160
|
+
while (true) {
|
|
161
|
+
const answer = (await prompt.question("Select an agent [1-2]: "))
|
|
162
|
+
.trim()
|
|
163
|
+
.toLowerCase();
|
|
164
|
+
if (answer === "1" || answer === "codex") return "codex";
|
|
165
|
+
if (answer === "2" || answer === "claude") return "claude";
|
|
166
|
+
process.stdout.write("Enter 1 for Codex or 2 for Claude.\n");
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
prompt.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @param {string} target */
|
|
174
|
+
async function pathExists(target) {
|
|
175
|
+
try {
|
|
176
|
+
await lstat(target);
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") return false;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { parseApplicationResponse } from "./application-response.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{ appKey: string }} options
|
|
7
|
+
* @param {{ api: { getApp(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
|
|
8
|
+
*/
|
|
9
|
+
export async function getApp(options, deps) {
|
|
10
|
+
const accessToken =
|
|
11
|
+
await deps.sessions.loadToken();
|
|
12
|
+
|
|
13
|
+
const result =
|
|
14
|
+
await deps.api.getApp(accessToken, options.appKey);
|
|
15
|
+
|
|
16
|
+
const app =
|
|
17
|
+
parseApplicationResponse(result.body, options.appKey);
|
|
18
|
+
|
|
19
|
+
deps.log(JSON.stringify({ data: app }, null, 2));
|
|
20
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { CliError } from "../errors.mjs";
|
|
4
|
+
import { getCommandInvocation } from "../config.mjs";
|
|
5
|
+
import { invalidResponse, parseApplication, responseObject } from "./application-response.mjs";
|
|
6
|
+
|
|
7
|
+
const HEADERS = ["APP KEY", "NAME", "STATUS"];
|
|
8
|
+
const MAX_PAGES = 100;
|
|
9
|
+
|
|
10
|
+
/** @typedef {{ appKey: string, name: string, status: string, createdAt: string, updatedAt: string }} App */
|
|
11
|
+
|
|
12
|
+
/**
|
|
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
|
|
15
|
+
*/
|
|
16
|
+
export async function listApps(options, deps) {
|
|
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
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.json) {
|
|
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.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {unknown} body @returns {{ data: App[], nextCursor: string | null }} */
|
|
60
|
+
function parseApps(body) {
|
|
61
|
+
const {data, nextCursor} = responseObject(body);
|
|
62
|
+
|
|
63
|
+
if (!Array.isArray(data)) {
|
|
64
|
+
throw new CliError("The applications response did not contain a data array", {
|
|
65
|
+
code: "INVALID_RESPONSE",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (nextCursor !== null && (typeof nextCursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(nextCursor))) {
|
|
70
|
+
throw new CliError("The applications response contained an invalid nextCursor", {
|
|
71
|
+
code: "INVALID_RESPONSE",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rebuild every item from approved metadata so unexpected API fields never reach output.
|
|
76
|
+
const apps = data.map(entry => parseApplication(entry));
|
|
77
|
+
|
|
78
|
+
return { data: apps, nextCursor };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {App[]} apps @returns {string[]} */
|
|
82
|
+
function formatTable(apps) {
|
|
83
|
+
const rows = [HEADERS, ...apps.map((app) =>
|
|
84
|
+
[app.appKey, app.name, app.status])];
|
|
85
|
+
|
|
86
|
+
const widths = HEADERS.map((_, column) =>
|
|
87
|
+
Math.max(...rows.map((row) => row[column].length)));
|
|
88
|
+
|
|
89
|
+
return rows.map((row) =>
|
|
90
|
+
row
|
|
91
|
+
.map((cell, column) => (column === HEADERS.length - 1 ? cell : cell.padEnd(widths[column])))
|
|
92
|
+
.join(" "),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -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
|
+
}
|