@finebi/finebi-cli 1.2.0

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 ADDED
@@ -0,0 +1,312 @@
1
+ # FineBI CLI
2
+
3
+ FineBI CLI is a dynamic command line client for FineBI plugin capabilities. It handles local config, username/password login, capability cache, `--help/-h`, and HTTP command forwarding.
4
+
5
+ Array options take a JSON array as one argument, for example `--values '["上海","江苏"]'` for geographic matching. The CLI parses the array and validates its item schema before sending the request; comma-separated text is not an array.
6
+
7
+ ## Install
8
+
9
+ ```powershell
10
+ npm install -g @finebi/finebi-cli --registry=https://registry.npmjs.org/
11
+ ```
12
+
13
+ For local development, prefer `npm link` from this `cli` directory:
14
+
15
+ ```powershell
16
+ cd D:\project\ino-7.0\nuclear-all\plugin-bi-finecli\cli
17
+ npm link
18
+ finebi-cli --help
19
+ ```
20
+
21
+ After `npm link`, edits under `src` are picked up by the global `finebi-cli` command without linking again. If `finebi-cli` was installed from a packed `.tgz`, rebuild and reinstall the package after code changes:
22
+
23
+ ```powershell
24
+ cd D:\project\ino-7.0\nuclear-all\plugin-bi-finecli\cli
25
+ npm run pack:cli
26
+ npm install -g .\finebi-finebi-cli-1.0.0.tgz
27
+ ```
28
+
29
+ On Windows, use `where finebi-cli` if the command does not appear to update. Multiple Node.js/npm installations can leave more than one global command shim on `PATH`.
30
+
31
+ ## Publish an Update
32
+
33
+ Publish from this `cli` directory. A published npm version is immutable, so increment the version before every release. Make and commit the functional changes first, then run the following release checks and version update:
34
+
35
+ ```powershell
36
+ cd D:\project\ino-7.0\nuclear-all\plugin-bi-finecli\cli
37
+ npm test
38
+ npm run verify:pack
39
+ npm version patch --no-git-tag-version
40
+ ```
41
+
42
+ Use `patch` for bug fixes, `minor` for backward-compatible features, and `major` for breaking changes. `npm version` updates both `package.json` and `package-lock.json`. Review and commit the release version with a Chinese Conventional Commit message, then log in with an npm account authorized to publish under `@finebi`. The package publishes publicly to the official npm registry:
43
+
44
+ ```powershell
45
+ git add package.json package-lock.json
46
+ git commit -m "chore(cli): 发布 1.0.1"
47
+ npm login --registry=https://registry.npmjs.org/
48
+ npm publish --registry=https://registry.npmjs.org/ --access public
49
+ ```
50
+
51
+ After a successful publish, verify the registry version and install it. Public package installation does not require npm authentication:
52
+
53
+ ```powershell
54
+ npm view @finebi/finebi-cli version --registry=https://registry.npmjs.org/
55
+ npm install -g @finebi/finebi-cli@latest --registry=https://registry.npmjs.org/
56
+ finebi-cli version
57
+ ```
58
+
59
+ ## Testing
60
+
61
+ Run these commands from the repository root. On Windows PowerShell use `npm.cmd`; on Linux/macOS use `npm`:
62
+
63
+ ```powershell
64
+ npm.cmd --prefix cli test
65
+ npm.cmd --prefix cli run test:integration:stub
66
+ ```
67
+
68
+ Linux/macOS:
69
+
70
+ ```bash
71
+ npm --prefix cli test
72
+ npm --prefix cli run test:integration:stub
73
+ ```
74
+
75
+ For a real FineBI environment, use the corresponding integration script:
76
+
77
+ Windows PowerShell:
78
+
79
+ ```powershell
80
+ npm.cmd --prefix cli run test:integration:readonly
81
+ npm.cmd --prefix cli run test:integration:mutating
82
+ npm.cmd --prefix cli run test:integration:real
83
+ ```
84
+
85
+ Linux/macOS:
86
+
87
+ ```bash
88
+ npm --prefix cli run test:integration:readonly
89
+ npm --prefix cli run test:integration:mutating
90
+ npm --prefix cli run test:integration:real
91
+ ```
92
+
93
+ ### Real integration prerequisites
94
+
95
+ Before running a real FineBI test, configure all of the following:
96
+
97
+ - `cli/tests/fixtures/finebi-integration-environment.json`: set `ready` to `true`, and provide a reachable HTTP(S) `baseUrl`, the test `username`, a positive `revision`, and `passwordSource` pointing to `FINEBI_INTEGRATION_PASSWORD`. `reportDir` may be a relative default or an absolute path.
98
+ - `cli/tests/fixtures/capabilities/`: keep the per-resource snapshots valid and synchronized with the deployed plugin/environment. The commands and request schemas in real cases are checked against this Capability snapshot (aggregated by `fixtures/capabilities/load.js`).
99
+ - `cli/tests/real-cases/*.json`: enable at least one case with `enabled: true`. Readonly cases must use `mutation: false` commands; mutating cases must declare scoped cleanup for resources created by that case.
100
+ - FineBI: deploy the plugin version corresponding to the current source and verify that the configured account can log in and access the required resources. Run mutating cases only in a dedicated test environment.
101
+ - Credentials: inject the password through the environment variable before starting the test. `xxxx` below is only a placeholder and must be replaced locally or by CI Secret injection.
102
+
103
+ Windows PowerShell:
104
+
105
+ ```powershell
106
+ $env:FINEBI_INTEGRATION_PASSWORD = 'xxxx'
107
+ ```
108
+
109
+ Linux/macOS:
110
+
111
+ ```bash
112
+ export FINEBI_INTEGRATION_PASSWORD='xxxx'
113
+ ```
114
+
115
+ Do not commit the real value or place it in case JSON, CLI arguments, logs, or reports. If `--report-dir` is supplied, it must be an existing writable absolute directory; `--case` can narrow the run to one enabled case. See the complete [CLI testing guide](../docs/cli-testing/20260804-finebi-real-integration/cli-testing-guide.md) for platform-specific setup, real-case authoring with the `finebi-cli-case-authoring` skill, cleanup rules, and report troubleshooting.
116
+
117
+ ## Usage
118
+
119
+ Built-in help is always available and does not require server config, login, or a capability refresh:
120
+
121
+ ```powershell
122
+ finebi-cli --help
123
+ finebi-cli --help --json
124
+ finebi-cli config set --help
125
+ finebi-cli auth login --help
126
+ finebi-cli capability refresh --help
127
+ finebi-cli version --help
128
+ finebi-cli capabilities --help
129
+ finebi-cli config --help
130
+ finebi-cli auth --help
131
+ finebi-cli capability --help
132
+ ```
133
+
134
+ Dynamic resource help, such as `finebi-cli <resource> --help`, is loaded from the FineBI capability cache. Login refreshes this cache automatically:
135
+
136
+ ```powershell
137
+ finebi-cli config set base-url http://localhost:37799/webroot/decision
138
+ finebi-cli auth login --username 1
139
+ finebi-cli version
140
+ finebi-cli --help
141
+ finebi-cli <resource> --help
142
+ finebi-cli <resource> <command> [options]
143
+ ```
144
+
145
+ Run `finebi-cli capability refresh` manually when backend commands changed, the cache expired, or you want to force a reload.
146
+
147
+ ### Multiple environments
148
+
149
+ Create an environment by logging in with its name and address:
150
+
151
+ ```powershell
152
+ finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser
153
+ finebi-cli auth login --profile prod --base-url https://prod.example.com/webroot/decision --username admin --password-stdin
154
+ finebi-cli auth list
155
+ finebi-cli auth use test
156
+ finebi-cli capability info --profile prod
157
+ finebi-cli auth info --profile prod
158
+ finebi-cli config set timeout-ms 60000 --profile prod
159
+ finebi-cli auth logout --profile prod
160
+ finebi-cli auth remove prod
161
+ ```
162
+
163
+ Successful login selects that environment as the default; add `--no-use` to keep the existing default. Login to an existing name reuses its address unless overridden. A new name requires `--base-url` or `FINEBI_BASE_URL` and never inherits another environment's identity. Failed authentication leaves existing environments and the default unchanged.
164
+
165
+ Selection order is `--profile` > `FINEBI_PROFILE` > saved default. The option works before or after the command. Ordinary commands never create missing environments or change the default. Names are case-sensitive, 1–64 ASCII letters/digits/underscores/hyphens, beginning with a letter or digit. `auth list` shows local metadata only, not a live authentication check. JSON responses identify the selected environment in `meta.profile`.
166
+
167
+ Each environment has separate credentials, connection settings and capabilities. Changing its saved `base-url` clears its credentials and cache. Temporary URL/token overrides do not change saved configuration; a different URL never inherits saved credentials. Requests with temporary credentials fetch capabilities for that invocation without overwriting a saved cache. Explicit session tokens take precedence over saved Access Keys.
168
+
169
+ `auth logout` revokes the selected environment's saved Key and keeps its connection settings. `auth remove <name>` and `config clear --profile <name>` delete only that local environment and cache, without revoking server credentials. Removing the default does not automatically select another saved environment. Persistent configuration and logout operations use the selected saved environment, not connection override variables.
170
+
171
+ Existing single-environment configuration is exposed as `default`; read-only commands do not rewrite it. Multi-environment writes migrate it while preserving its credentials and default capability cache. See the [command contract](../docs/commands/README.md#多环境管理) for the complete behavior.
172
+
173
+ ## Access Key
174
+
175
+ After a successful `auth login`, the CLI automatically issues a long-lived Access Key so commands keep working after the short-lived session token (JWT) expires:
176
+
177
+ - Login provisions a key with description `finebi-cli@<hostname>` for `default`, or `finebi-cli@<hostname>/<profile>` for a named environment, replacing stale keys with the same description. The plaintext key is stored in the environment's `auth.accessKey` and never printed. On POSIX systems, the CLI enforces mode `0700` on the config directory and `0600` on `config.json`. On Windows, it removes inherited ACLs and grants access only to the current user, SYSTEM, and Administrators; failures emit a security warning.
178
+ - Business commands send `X-FineBI-CLI-Access-Key` when a key is present, and fall back to the session token otherwise.
179
+ - Add `--no-access-key` to `auth login` to skip issuance and stay on the session token.
180
+ - `auth info` shows `authMode` (`access-key` / `session-token` / `none`) with both the session token and Access Key masked.
181
+ - `auth logout` revokes the server-side key via `self-revoke` (works even after the JWT expired), then clears local credentials. If revocation fails, credentials are preserved for retry; use `auth logout --force` only when local credentials must be removed without confirmed server revocation.
182
+
183
+ ```powershell
184
+ finebi-cli auth login --username 1
185
+ finebi-cli auth login --username 1 --no-access-key
186
+ finebi-cli auth info
187
+ finebi-cli auth logout
188
+ finebi-cli auth logout --force
189
+ ```
190
+
191
+ Agent discovery flow:
192
+
193
+ ```powershell
194
+ finebi-cli --help --json
195
+ finebi-cli config set base-url http://localhost:37799/webroot/decision
196
+ finebi-cli auth login --from-env
197
+ finebi-cli <resource> --help --json
198
+ finebi-cli <resource> <command> [options] --json
199
+ ```
200
+
201
+ For complex file-based resources, do not expect command help to contain a complete source example. `widget template` returns a Widget JS DSL source template selected by chart type and scenario; `dashboard template` returns a Dashboard JS DSL example script with named placeholders and a variable contract. Their `add` and `update` commands accept JS DSL source files. Other resources may still expose writable JSON templates according to their dynamic command help.
202
+
203
+ ## Agent Skills
204
+
205
+ Use the `finebi-cli` skill when an Agent needs to operate this CLI. The skill should follow the discovery flow above and treat dynamic command help as the source of truth after capability refresh.
206
+
207
+ Use the `finebi-cli-command-designer` skill for command design work: new command groups, schema changes, response contracts, help output, and design markdown. Command-specific business rules should be designed in the command document and implemented on the backend; the CLI should only interpret field-level rules declared in the capability schema.
208
+
209
+ ## Output
210
+
211
+ FineBI CLI keeps two audiences in mind:
212
+
213
+ - Human-facing built-in commands print concise text by default.
214
+ - Agent and script integrations can add `--json` to get the full JSON envelope.
215
+ - Dynamic business commands use backend capability display hints for human-readable output by default.
216
+ - Dynamic business commands can use `-f, --format` for display formats supported by the command shape.
217
+ - `--json` has priority over display formats and always returns the full JSON envelope.
218
+
219
+ Large responses can be narrowed before they reach the caller (all three are CLI-side only — they are
220
+ stripped from the command arguments and never forwarded to the plugin):
221
+
222
+ - `--select <paths>` projects the response onto comma-separated dotted paths (arrays use indexes,
223
+ e.g. `data.dsl,warnings,errors.0.message`). Output is always JSON and keeps `ok` / `command`;
224
+ an unknown path fails loud with the available top-level keys instead of printing a partial result.
225
+ - `--max-chars <n>` keeps the head and tail of long output with explicit truncation markers
226
+ (total/omitted character counts plus how to get the full text). The cap is inclusive: markers
227
+ count against it, so the truncated output never exceeds `n` characters.
228
+ - `--save <path>` writes the full JSON envelope (projected when `--select` is used — the human
229
+ rendering is lossy, so files always hold the complete response) and prints only a short summary:
230
+ `ok` / `command` / `saved` / `chars` / `warnings`. `--save` wins over `--max-chars` (the summary
231
+ is already tiny); a failed write reports `INVALID_ARGUMENT` with the path instead of a stack trace.
232
+
233
+ A command that fails is never projected (its `data` is null, so projection would hide the real error
234
+ behind `Unknown path(s)`); `--select` implies JSON output, including for its own argument errors.
235
+ Failed commands are still saved when `--save` is present — the file holds the failure envelope, so
236
+ scripted consumers never read a stale success from a previous run.
237
+
238
+ ```powershell
239
+ finebi-cli dashboard dsl --id <id> --select data.dsl,warnings
240
+ finebi-cli dashboard bean --id <id> --save temp/board.json
241
+ finebi-cli subject info --id <id> --json --max-chars 8000
242
+ ```
243
+
244
+ Human-friendly defaults:
245
+
246
+ ```powershell
247
+ finebi-cli version
248
+ finebi-cli config get
249
+ finebi-cli auth info
250
+ finebi-cli capability info
251
+ finebi-cli config clear
252
+ finebi-cli auth logout
253
+ finebi-cli capability clear-cache
254
+ ```
255
+
256
+ Structured output:
257
+
258
+ ```powershell
259
+ finebi-cli version --json
260
+ finebi-cli config get --json
261
+ finebi-cli auth info --json
262
+ finebi-cli capability info --json
263
+ finebi-cli <resource> <command> [options] --json
264
+ ```
265
+
266
+ Dynamic display formats:
267
+
268
+ ```powershell
269
+ finebi-cli <resource> <command> [options] -f auto
270
+ finebi-cli <resource> <command> [options] -f table
271
+ finebi-cli <resource> <command> [options] -f expanded
272
+ finebi-cli <resource> <command> [options] -f csv
273
+ ```
274
+
275
+ Dynamic shapes are `record-list`, `detail`, `tabular-preview`, and `generic`. Java capability metadata declares only the shape and the default format used by `auto`. Node.js renders these shapes generically from the response data, preserving the returned information instead of hardcoding business command names or command-specific columns.
276
+
277
+ Environment login:
278
+
279
+ ```powershell
280
+ $env:FINEBI_BASE_URL="http://localhost:37799/webroot/decision"
281
+ $env:FINEBI_USERNAME="1"
282
+ $env:FINEBI_PASSWORD="your-password"
283
+ finebi-cli auth login --from-env
284
+ ```
285
+
286
+ Successful login also attempts to refresh the dynamic capability cache. If the refresh fails, login still succeeds and the CLI prints a warning with `finebi-cli capability refresh` as the follow-up action.
287
+
288
+ Browser authorization login:
289
+
290
+ ```powershell
291
+ finebi-cli auth login --browser
292
+ ```
293
+
294
+ This prints a same-server confirmation URL and code to stderr. Sign in through the existing FineBI browser entry first, then open the URL and confirm the code. The CLI only sends the browser authorization request fields and saves the returned Access Key after approval; it does not send an existing local credential to the authorization endpoints.
295
+
296
+ Pipe password through stdin:
297
+
298
+ ```powershell
299
+ echo your-password | finebi-cli auth login --username 1 --password-stdin
300
+ ```
301
+
302
+ ## Local Files
303
+
304
+ - Config: `%USERPROFILE%\.finebi-cli\config.json`
305
+ - Capability cache: `%USERPROFILE%\.finebi-cli\capabilities.json`
306
+ - Named environment caches: `%USERPROFILE%\.finebi-cli\capabilities-profile-<hex-encoded-name>.json` (`default` uses `capabilities.json`).
307
+
308
+ Multi-environment `config.json` stores `schemaVersion: 2`, `currentProfile` (nullable), and a `profiles` object keyed by environment name. Writes are serialized with a short-lived `config.json.lock` and atomically replace the configuration file. A lock timeout reports an error without modifying the file.
309
+
310
+ Concurrent logins to the same environment are rejected using `config.json.login-<hex-encoded-name>.lock`; different environments may log in concurrently. Locks are released when the operation settles. If a process is forcibly terminated and leaves a lock behind, verify that the owning operation has stopped before manually removing that lock file.
311
+
312
+ Passwords are not persisted by the CLI.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@finebi/finebi-cli",
3
+ "version": "1.2.0",
4
+ "description": "FineBI dynamic capability command line client.",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org/",
8
+ "access": "public"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "finebi-cli": "./src/index.js"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.17"
20
+ },
21
+ "scripts": {
22
+ "test": "npm run test:unit && npm run test:process && npm run test:contract && npm run test:integration:stub",
23
+ "test:unit": "node --test tests/unit/access-key.test.js tests/unit/auth.test.js tests/unit/capability.test.js tests/unit/config-permissions.test.js tests/unit/config.test.js tests/unit/dynamic-command-forwarding.test.js tests/unit/dynamic-command-multipart.test.js tests/unit/dynamic-command-preflight.test.js tests/unit/error-catalog-loader.test.js tests/unit/error-catalog.test.js tests/unit/error-construction-guard.test.js tests/unit/help.test.js tests/unit/http-stub.test.js tests/unit/index-auth-login.test.js tests/unit/index-auth-session.test.js tests/unit/index-browser-auth.test.js tests/unit/index-config.test.js tests/unit/index-dynamic-errors.test.js tests/unit/index-dynamic-render.test.js tests/unit/index-help.test.js tests/unit/index-profiles.test.js tests/unit/output-limits.test.js tests/unit/output.test.js tests/unit/package.test.js tests/unit/schema-options-capability.test.js tests/unit/schema-options-file-input.test.js tests/unit/schema-options-scalar.test.js",
24
+ "test:process": "node --test tests/process/bench-cli-runner.test.js tests/process/process-cli.test.js tests/process/profiles-cli.test.js",
25
+ "test:contract": "node --test tests/contract/capability-contract.test.js",
26
+ "test:integration:stub": "node --test tests/integration/analysis-ds-workflows.test.js tests/integration/assertions.test.js tests/integration/auth-system.test.js tests/integration/case-loader.test.js tests/integration/cli-process.test.js tests/integration/config.test.js tests/integration/dashboard-workflows.test.js tests/integration/entry.test.js tests/integration/failure-security.test.js tests/integration/file-template.test.js tests/integration/mutating-file-input.test.js tests/integration/mutating-resource-ledger.test.js tests/integration/mutating-workflow-guards.test.js tests/integration/readonly-assertions.test.js tests/integration/readonly-file-input.test.js tests/integration/readonly-workflow-contract.test.js tests/integration/report-directory.test.js tests/integration/report-entry.test.js tests/integration/report-secrets.test.js tests/integration/report-summary-artifacts.test.js tests/integration/runner.test.js",
27
+ "test:integration:readonly": "node tests/integration/entry.js readonly",
28
+ "test:integration:mutating": "node tests/integration/entry.js mutating",
29
+ "test:integration:real": "node tests/integration/entry.js all",
30
+ "prebench": "npm test",
31
+ "bench": "node bench/src/runner.js",
32
+ "prepack:cli": "npm test",
33
+ "pack:cli": "npm pack",
34
+ "preverify:pack": "npm test",
35
+ "verify:pack": "npm pack --dry-run"
36
+ }
37
+ }
@@ -0,0 +1,2 @@
1
+ export const ACCESS_KEY_BASE_PATH = "/api/bi-cli/access-key";
2
+ export const ACCESS_KEY_HEADER_NAME = "X-BI-CLI-Access-Key";
@@ -0,0 +1,174 @@
1
+ import {failure, success} from "./output.js";
2
+ import {ACCESS_KEY_BASE_PATH} from "./access-key-contract.js";
3
+ import {authenticationFailure} from "./auth-failure.js";
4
+ import {buildAuthHeaders} from "./auth-headers.js";
5
+ import {cliError} from "./errors.js";
6
+
7
+
8
+ /**
9
+ * 创建 Access Key。使用 JWT 登录态(Cookie/Bearer)认证,服务端据 Cookie 识别当前用户。
10
+ * 校验响应中含 token/id,防止契约漂移时把无效响应当成功。
11
+ */
12
+ export async function createAccessKey(options) {
13
+ const query = options.description ? `?description=${encodeURIComponent(options.description)}` : "";
14
+ const envelope = await sendAccessKeyRequest({
15
+ ...options,
16
+ command: "access-key.create",
17
+ method: "POST",
18
+ endpoint: `${ACCESS_KEY_BASE_PATH}/create${query}`,
19
+ });
20
+ if (!envelope.ok) {
21
+ return envelope;
22
+ }
23
+ const key = envelope.data;
24
+ if (!key || !key.token || !key.id) {
25
+ return failure("access-key.create", cliError("ACCESS_KEY_CREATE_INVALID", {
26
+ path: "data.token",
27
+ message: "Access Key create response did not contain a token.",
28
+ hint: "Check the FineBI CLI plugin version and access-key endpoint contract.",
29
+ retryable: false,
30
+ nextActions: ["finebi-cli auth info"],
31
+ }));
32
+ }
33
+ return success("access-key.create", key);
34
+ }
35
+
36
+ /**
37
+ * 列出当前用户(管理员为全部)的 Access Key。响应不含 token 明文。
38
+ */
39
+ export async function listAccessKeys(options) {
40
+ return sendAccessKeyRequest({
41
+ ...options,
42
+ command: "access-key.list",
43
+ method: "GET",
44
+ endpoint: `${ACCESS_KEY_BASE_PATH}/list`,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * 按 id 删除 Access Key。
50
+ */
51
+ export async function deleteAccessKey(options) {
52
+ return sendAccessKeyRequest({
53
+ ...options,
54
+ command: "access-key.delete",
55
+ method: "POST",
56
+ endpoint: `${ACCESS_KEY_BASE_PATH}/delete?id=${encodeURIComponent(options.id)}`,
57
+ });
58
+ }
59
+
60
+ /**
61
+ * 自撤销:用 X-BI-CLI-Access-Key 头调 /self-revoke 删除自己出示的这把 key。
62
+ * 认证方式为「出示 key 即证明持有」,不依赖 JWT 登录态,适用于 JWT 过期后 logout 撤销。
63
+ */
64
+ export async function selfRevokeAccessKey(options) {
65
+ return sendAccessKeyRequest({
66
+ ...options,
67
+ command: "access-key.self-revoke",
68
+ method: "POST",
69
+ endpoint: `${ACCESS_KEY_BASE_PATH}/self-revoke`,
70
+ });
71
+ }
72
+
73
+ /**
74
+ * 删旧建新:列出同 description 的旧 Key 并删除,再创建一把新 Key。
75
+ * list 失败时停止签发,避免无法确认旧 Key 状态却继续创建而产生重复;旧 Key 删除失败逐条告警。
76
+ */
77
+ export async function provisionAccessKey(options) {
78
+ const warnings = [];
79
+ const listed = await listAccessKeys(options);
80
+ if (listed.ok) {
81
+ const existing = Array.isArray(listed.data?.tokens) ? listed.data.tokens : [];
82
+ const stale = existing.filter((item) => isStaleKeyForCurrentUser(item, options));
83
+ for (const item of stale) {
84
+ const removed = await deleteAccessKey({ ...options, id: item.id });
85
+ if (!removed.ok) {
86
+ warnings.push(cliError("ACCESS_KEY_STALE_DELETE_FAILED", {
87
+ message: `Failed to delete a stale Access Key (id=${item.id}).`,
88
+ id: item.id,
89
+ cause: removed.errors?.[0],
90
+ }));
91
+ }
92
+ }
93
+ } else {
94
+ warnings.push(cliError("ACCESS_KEY_LIST_FAILED", {
95
+ message: "Could not list existing Access Keys; issuance was stopped to avoid creating a duplicate.",
96
+ cause: listed.errors?.[0],
97
+ }));
98
+ return { created: listed, warnings };
99
+ }
100
+ const created = await createAccessKey(options);
101
+ return { created, warnings };
102
+ }
103
+
104
+ function isStaleKeyForCurrentUser(item, options) {
105
+ if (!options.username || !item?.id || item.description !== options.description) {
106
+ return false;
107
+ }
108
+ // 管理员 list 可能返回所有用户;缺少当前用户名时宁可保留旧 Key,也不能误删他人的 Key。
109
+ return item.username === options.username;
110
+ }
111
+
112
+ async function sendAccessKeyRequest(options) {
113
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
114
+ const controller = new AbortController();
115
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
116
+ try {
117
+ const response = await (options.fetchImpl || fetch)(`${baseUrl}${options.endpoint}`, {
118
+ method: options.method,
119
+ headers: buildAuthHeaders(options),
120
+ signal: controller.signal,
121
+ });
122
+ return await parseAccessKeyResponse(response, options.command, options.endpoint);
123
+ } catch (error) {
124
+ return failure(options.command, cliError("NETWORK_ERROR", {
125
+ path: options.endpoint,
126
+ message: error instanceof Error ? error.message : String(error),
127
+ hint: "Check the FineBI base URL and network connection.",
128
+ retryable: true,
129
+ nextActions: ["finebi-cli auth info"],
130
+ }));
131
+ } finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+
136
+ async function parseAccessKeyResponse(response, command, path) {
137
+ const text = await response.text();
138
+ if (response.ok && (response.status === 204 || text.trim() === "")) {
139
+ return success(command, null);
140
+ }
141
+ const contentType = response.headers.get("content-type") || "";
142
+ if (!contentType.includes("json") || text.trim().startsWith("<")) {
143
+ return failure(command, cliError("AUTH_REDIRECT_OR_HTML_RESPONSE", {
144
+ path,
145
+ message: "FineBI returned an HTML login or redirect page instead of JSON.",
146
+ hint: "Run finebi-cli auth login and retry.",
147
+ retryable: true,
148
+ nextActions: ["finebi-cli auth login"],
149
+ }), { status: response.status });
150
+ }
151
+ if (!response.ok) {
152
+ if (response.status === 401 || response.status === 403) {
153
+ return authenticationFailure(command, path, response.status);
154
+ }
155
+ return failure(command, cliError("HTTP_ERROR", {
156
+ path,
157
+ message: `FineBI returned HTTP ${response.status}: ${text.slice(0, 300)}`,
158
+ hint: "Check FineBI server logs and access-key endpoint availability.",
159
+ retryable: false,
160
+ nextActions: ["finebi-cli auth info"],
161
+ }), { status: response.status });
162
+ }
163
+ const payload = JSON.parse(text);
164
+ if (payload && payload.errorCode) {
165
+ return failure(command, cliError("ACCESS_KEY_ERROR", {
166
+ path,
167
+ message: payload.errorMsg || String(payload.errorCode),
168
+ hint: "Check the username, permissions, and access-key endpoint.",
169
+ retryable: false,
170
+ nextActions: ["finebi-cli auth info"],
171
+ }), { status: response.status });
172
+ }
173
+ return success(command, payload.data ?? null);
174
+ }
@@ -0,0 +1,18 @@
1
+ import {cliError} from "./errors.js";
2
+ import {failure} from "./output.js";
3
+
4
+ /**
5
+ * 将 401/403 统一映射为可恢复的认证错误。
6
+ *
7
+ * Access Key 被撤销或失效时,业务接口与 capability 接口必须给出相同的重新签发入口,
8
+ * 不能落入“检查服务端日志”一类无法自助恢复的通用 HTTP 提示。
9
+ */
10
+ export function authenticationFailure(command, path, status) {
11
+ return failure(command, cliError("AUTHENTICATION_FAILED", {
12
+ path,
13
+ message: `FineBI rejected the authentication credentials with HTTP ${status}.`,
14
+ hint: "Run finebi-cli auth login and retry.",
15
+ retryable: true,
16
+ nextActions: ["finebi-cli auth login"],
17
+ }), { status });
18
+ }
@@ -0,0 +1,21 @@
1
+ import { ACCESS_KEY_HEADER_NAME } from "./access-key-contract.js";
2
+
3
+ /**
4
+ * 统一构造 FineBI CLI 认证请求头。
5
+ *
6
+ * Access Key 与平台登录态互斥:一旦存在 CLI Access Key,只发送插件专属 Header;
7
+ * 否则才回退到 Bearer + Cookie,避免调用方各自实现后产生双认证或旧 Header 漂移。
8
+ */
9
+ export function buildAuthHeaders({ token, accessKey, contentType }) {
10
+ const headers = { Accept: "application/json" };
11
+ if (contentType) {
12
+ headers["Content-Type"] = contentType;
13
+ }
14
+ if (accessKey) {
15
+ headers[ACCESS_KEY_HEADER_NAME] = accessKey;
16
+ } else if (token) {
17
+ headers.Authorization = `Bearer ${token}`;
18
+ headers.Cookie = `fine_auth_token=${token}`;
19
+ }
20
+ return headers;
21
+ }