@finebi/finebi-cli 1.2.1 → 1.4.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.
Files changed (67) hide show
  1. package/README.md +63 -39
  2. package/package.json +40 -37
  3. package/skills/finebi-cli-agent/SKILL.md +133 -0
  4. package/skills/finebi-cli-agent/agents/openai.yaml +7 -0
  5. package/skills/finebi-cli-agent/references/analysis-dsl-api.md +322 -0
  6. package/skills/finebi-cli-agent/references/artifact-ordering.md +29 -0
  7. package/skills/finebi-cli-agent/references/command-workflows.md +199 -0
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +1037 -0
  9. package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +195 -0
  10. package/skills/finebi-cli-agent/references/file-to-dashboard.md +65 -0
  11. package/skills/finebi-cli-agent/references/formula-basics.md +102 -0
  12. package/skills/finebi-cli-agent/references/formula-contexts.md +66 -0
  13. package/skills/finebi-cli-agent/references/formula-functions.md +247 -0
  14. package/skills/finebi-cli-agent/references/formula-generation.md +121 -0
  15. package/skills/finebi-cli-agent/references/formula-glossary.md +17 -0
  16. package/skills/finebi-cli-agent/references/formula-help-boundaries.md +87 -0
  17. package/skills/finebi-cli-agent/references/formula-help-index.md +264 -0
  18. package/skills/finebi-cli-agent/references/formula-parameters-aggregate.md +143 -0
  19. package/skills/finebi-cli-agent/references/formula-parameters-analysis.md +223 -0
  20. package/skills/finebi-cli-agent/references/formula-parameters-date.md +236 -0
  21. package/skills/finebi-cli-agent/references/formula-parameters-math-logic.md +271 -0
  22. package/skills/finebi-cli-agent/references/formula-parameters-text.md +217 -0
  23. package/skills/finebi-cli-agent/references/formula-recipes-analysis.md +124 -0
  24. package/skills/finebi-cli-agent/references/formula-recipes-text-date.md +86 -0
  25. package/skills/finebi-cli-agent/references/formula-semantics.md +39 -0
  26. package/skills/finebi-cli-agent/references/formula-syntax.md +70 -0
  27. package/skills/finebi-cli-agent/references/formula-troubleshooting.md +63 -0
  28. package/skills/finebi-cli-agent/references/formula-validation.md +36 -0
  29. package/skills/finebi-cli-agent/references/formula.md +37 -0
  30. package/skills/finebi-cli-agent/references/handoff-contract.md +210 -0
  31. package/skills/finebi-cli-agent/references/mapping-rules.md +423 -0
  32. package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +150 -0
  33. package/skills/finebi-cli-agent/references/scenario-workflows.md +75 -0
  34. package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +457 -0
  35. package/skills/finebi-cli-agent/references/widget-dsl-api.md +1340 -0
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +11 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key-contract.js +2 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key.js +174 -0
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-failure.js +18 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-headers.js +21 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth.js +116 -0
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/src/branding.js +31 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +331 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/src/capability.js +122 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config-permissions.js +87 -0
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +189 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/src/dynamic-command.js +324 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +482 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/src/errors.js +22 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +467 -0
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +1351 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -0
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/src/output-limits.js +188 -0
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/src/output.js +33 -0
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -0
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-options.js +265 -0
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-validator.js +194 -0
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli-runner-core.mjs +399 -0
  60. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +29 -0
  61. package/src/config.js +27 -27
  62. package/src/error-catalog.js +12 -0
  63. package/src/help.js +70 -55
  64. package/src/index.js +17 -4
  65. package/src/install-skill.js +89 -0
  66. package/src/profiles.js +158 -158
  67. package/src/skill-files.js +94 -0
@@ -0,0 +1,331 @@
1
+ import {spawn} from "node:child_process";
2
+ import {platform} from "node:process";
3
+
4
+ import {selfRevokeAccessKey} from "./access-key.js";
5
+ import {cliError} from "./errors.js";
6
+ import {failure, success} from "./output.js";
7
+
8
+ const BROWSER_AUTH_BASE_PATH = "/api/bi-cli/browser-auth";
9
+
10
+ export async function authorizeWithBrowser(options) {
11
+ if (options.signal?.aborted) {
12
+ return abortedAuthorization();
13
+ }
14
+ const base = validateBaseUrl(options.baseUrl);
15
+ if (!base.ok) {
16
+ return base;
17
+ }
18
+ const started = await postJson({
19
+ ...options,
20
+ command: "auth.login",
21
+ endpoint: `${BROWSER_AUTH_BASE_PATH}/start`,
22
+ body: {description: options.description},
23
+ });
24
+ if (!started.ok) {
25
+ return started;
26
+ }
27
+ const session = started.data;
28
+ if (!isStartResponse(session)) {
29
+ return failure("auth.login", cliError("BROWSER_AUTH_START_INVALID", {
30
+ path: "data",
31
+ message: "Browser authorization start response is incomplete.",
32
+ nextActions: ["finebi-cli auth login --browser"],
33
+ }));
34
+ }
35
+ const verificationUrl = verificationUrlFor(base.data, session.verificationPath);
36
+ if (!verificationUrl) {
37
+ return failure("auth.login", cliError("BROWSER_AUTH_START_INVALID", {
38
+ path: "data.verificationPath",
39
+ message: "Browser authorization response returned an unsafe verification path.",
40
+ nextActions: ["finebi-cli auth login --browser"],
41
+ }));
42
+ }
43
+ report(options, `Open this URL after signing in to FineBI:\n${verificationUrl}\nConfirmation code: ${session.userCode}`);
44
+ if (options.interactive) {
45
+ try {
46
+ await (options.openBrowser || openBrowser)(verificationUrl);
47
+ } catch (_error) {
48
+ report(options, "Could not open a browser automatically. Copy the URL above into a browser.");
49
+ }
50
+ }
51
+ const deadline = clock(options) + session.expiresIn * 1000;
52
+ let delayMs = session.interval * 1000;
53
+ while (clock(options) < deadline) {
54
+ if (options.signal?.aborted) {
55
+ return abortedAuthorization();
56
+ }
57
+ const polled = await postJson({
58
+ ...options,
59
+ command: "auth.login",
60
+ endpoint: `${BROWSER_AUTH_BASE_PATH}/poll`,
61
+ body: {deviceCode: session.deviceCode},
62
+ allowRateLimit: true,
63
+ });
64
+ if (!polled.ok) {
65
+ if (polled.meta?.status === 429) {
66
+ delayMs = retryDelay(polled.meta.retryAfter, delayMs);
67
+ if (await waitForPoll(options, delayMs)) {
68
+ return abortedAuthorization();
69
+ }
70
+ continue;
71
+ }
72
+ if (polled.errors?.[0]?.code === "NETWORK_ERROR") {
73
+ delayMs = retryDelay(null, delayMs);
74
+ if (await waitForPoll(options, delayMs)) {
75
+ return abortedAuthorization();
76
+ }
77
+ continue;
78
+ }
79
+ return polled;
80
+ }
81
+ const result = polled.data;
82
+ if (!result || typeof result.status !== "string") {
83
+ return failure("auth.login", cliError("BROWSER_AUTH_POLL_INVALID", {
84
+ path: "data.status",
85
+ message: "Browser authorization poll response did not contain a status.",
86
+ nextActions: ["finebi-cli auth login --browser"],
87
+ }));
88
+ }
89
+ if (result.status === "pending") {
90
+ if (await waitForPoll(options, delayMs)) {
91
+ return abortedAuthorization();
92
+ }
93
+ continue;
94
+ }
95
+ if (result.status === "approved") {
96
+ if (!result.token || !result.id) {
97
+ return failure("auth.login", cliError("BROWSER_AUTH_POLL_INVALID", {
98
+ path: "data.token",
99
+ message: "Approved browser authorization did not return an Access Key.",
100
+ nextActions: ["finebi-cli auth login --browser"],
101
+ }));
102
+ }
103
+ try {
104
+ await options.onApproved(result);
105
+ } catch (error) {
106
+ const revoked = await selfRevokeAccessKey({
107
+ baseUrl: base.data,
108
+ accessKey: result.token,
109
+ timeoutMs: options.timeoutMs,
110
+ fetchImpl: options.fetchImpl,
111
+ });
112
+ return failure("auth.login", cliError("BROWSER_AUTH_SAVE_FAILED", {
113
+ path: "auth.accessKey",
114
+ message: "Browser authorization succeeded but the new Access Key could not be saved locally.",
115
+ hint: revoked.ok
116
+ ? "The newly issued Access Key was revoked. Check local configuration permissions and try again."
117
+ : "The newly issued Access Key could not be saved or revoked. Revoke it manually by id.",
118
+ retryable: false,
119
+ nextActions: ["finebi-cli auth login --browser"],
120
+ partialResult: {id: result.id},
121
+ cause: error instanceof Error ? error.message : String(error),
122
+ }));
123
+ }
124
+ return success("auth.login", result);
125
+ }
126
+ const terminal = {
127
+ denied: ["BROWSER_AUTH_DENIED", "Browser authorization was denied."],
128
+ expired: ["BROWSER_AUTH_EXPIRED", "Browser authorization expired before approval."],
129
+ consumed: ["BROWSER_AUTH_CONSUMED", "Browser authorization was already consumed. Start a new authorization."],
130
+ }[result.status];
131
+ if (terminal) {
132
+ return failure("auth.login", cliError(terminal[0], {
133
+ path: "data.status",
134
+ message: terminal[1],
135
+ nextActions: ["finebi-cli auth login --browser"],
136
+ ...(result.id ? {partialResult: {id: result.id}} : {}),
137
+ }));
138
+ }
139
+ return failure("auth.login", cliError("BROWSER_AUTH_POLL_INVALID", {
140
+ path: "data.status",
141
+ message: `Browser authorization returned an unsupported status: ${result.status}.`,
142
+ nextActions: ["finebi-cli auth login --browser"],
143
+ }));
144
+ }
145
+ return failure("auth.login", cliError("BROWSER_AUTH_TIMEOUT", {
146
+ path: "data.status",
147
+ message: "Browser authorization timed out before approval.",
148
+ nextActions: ["finebi-cli auth login --browser"],
149
+ }));
150
+ }
151
+
152
+ function abortedAuthorization() {
153
+ return failure("auth.login", cliError("BROWSER_AUTH_ABORTED", {
154
+ path: "browserAuthorization",
155
+ message: "Browser authorization was aborted before approval.",
156
+ nextActions: ["finebi-cli auth login --browser"],
157
+ }));
158
+ }
159
+
160
+ function validateBaseUrl(value) {
161
+ try {
162
+ const url = new URL(value);
163
+ const localHttp = url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname.toLowerCase());
164
+ if (url.protocol !== "https:" && !localHttp) {
165
+ throw new Error("Browser authorization requires HTTPS, except for localhost development.");
166
+ }
167
+ if (url.username || url.password || url.search || url.hash) {
168
+ throw new Error("Browser authorization base URL must not contain credentials, a query, or a fragment.");
169
+ }
170
+ return success("auth.login", url.toString().replace(/\/+$/, ""));
171
+ } catch (error) {
172
+ return failure("auth.login", cliError("BROWSER_AUTH_URL_INVALID", {
173
+ path: "baseUrl",
174
+ message: error instanceof Error ? error.message : String(error),
175
+ nextActions: ["finebi-cli config set base-url <https-url>"],
176
+ }));
177
+ }
178
+ }
179
+
180
+ function isStartResponse(data) {
181
+ return data && typeof data.deviceCode === "string" && typeof data.userCode === "string"
182
+ && data.deviceCode.length > 0 && data.userCode.length > 0 && typeof data.verificationPath === "string"
183
+ && Number.isFinite(data.expiresIn) && data.expiresIn > 0 && data.expiresIn <= 600
184
+ && Number.isFinite(data.interval) && data.interval > 0 && data.interval <= 60;
185
+ }
186
+
187
+ function verificationUrlFor(baseUrl, verificationPath) {
188
+ if (!verificationPath.startsWith("/") || verificationPath.startsWith("//") || verificationPath.includes("\\")) {
189
+ return null;
190
+ }
191
+ const base = new URL(baseUrl);
192
+ const candidate = new URL(verificationPath, "https://browser-auth.invalid");
193
+ const decisionPath = base.pathname.replace(/\/+$/, "");
194
+ const expectedPath = `${BROWSER_AUTH_BASE_PATH}/authorize`;
195
+ if (candidate.origin !== "https://browser-auth.invalid" || candidate.username || candidate.password || candidate.hash
196
+ || candidate.pathname !== expectedPath) {
197
+ return null;
198
+ }
199
+ return new URL(`${decisionPath}${candidate.pathname}${candidate.search}`, base.origin).toString();
200
+ }
201
+
202
+ async function postJson(options) {
203
+ const controller = new AbortController();
204
+ const abortExternal = () => controller.abort();
205
+ options.signal?.addEventListener("abort", abortExternal, {once: true});
206
+ if (options.signal?.aborted) {
207
+ controller.abort();
208
+ }
209
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
210
+ try {
211
+ const response = await (options.fetchImpl || fetch)(`${options.baseUrl.replace(/\/+$/, "")}${options.endpoint}`, {
212
+ method: "POST",
213
+ headers: {"content-type": "application/json"},
214
+ body: JSON.stringify(options.body),
215
+ credentials: "omit",
216
+ redirect: "error",
217
+ signal: controller.signal,
218
+ });
219
+ if (options.allowRateLimit && response.status === 429) {
220
+ return failure(options.command, cliError("BROWSER_AUTH_RATE_LIMITED", {
221
+ path: options.endpoint,
222
+ message: "Browser authorization polling is being rate limited.",
223
+ retryable: true,
224
+ nextActions: ["Wait before polling again"],
225
+ }), {status: 429, retryAfter: response.headers.get("retry-after")});
226
+ }
227
+ if (!response.ok) {
228
+ return failure(options.command, cliError("BROWSER_AUTH_HTTP_ERROR", {
229
+ path: options.endpoint,
230
+ message: `FineBI returned HTTP ${response.status} from browser authorization.`,
231
+ retryable: response.status >= 500,
232
+ nextActions: ["finebi-cli auth login --browser"],
233
+ }), {status: response.status});
234
+ }
235
+ const text = await response.text();
236
+ let payload;
237
+ try {
238
+ payload = JSON.parse(text);
239
+ } catch (_error) {
240
+ return invalidSuccessResponse(options);
241
+ }
242
+ return success(options.command, payload?.data);
243
+ } catch (error) {
244
+ if (options.signal?.aborted) {
245
+ return abortedAuthorization();
246
+ }
247
+ return failure(options.command, cliError("NETWORK_ERROR", {
248
+ path: options.endpoint,
249
+ message: error instanceof Error ? error.message : String(error),
250
+ retryable: true,
251
+ nextActions: ["finebi-cli auth login --browser"],
252
+ }));
253
+ } finally {
254
+ clearTimeout(timer);
255
+ options.signal?.removeEventListener("abort", abortExternal);
256
+ }
257
+ }
258
+
259
+ function retryDelay(retryAfter, currentDelay) {
260
+ const seconds = Number.parseInt(retryAfter, 10);
261
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : Math.min(currentDelay * 2, 60000);
262
+ }
263
+
264
+ function wait(options, delayMs) {
265
+ return options.sleep ? options.sleep(delayMs) : new Promise((resolve) => setTimeout(resolve, delayMs));
266
+ }
267
+
268
+ async function waitForPoll(options, delayMs) {
269
+ if (!options.sleep && options.signal) {
270
+ if (options.signal.aborted) {
271
+ return true;
272
+ }
273
+ return new Promise((resolve) => {
274
+ const timer = setTimeout(done, delayMs, false);
275
+ const abort = () => done(true);
276
+ options.signal.addEventListener("abort", abort, {once: true});
277
+ function done(aborted) {
278
+ clearTimeout(timer);
279
+ options.signal.removeEventListener("abort", abort);
280
+ resolve(aborted);
281
+ }
282
+ });
283
+ }
284
+ if (!options.signal) {
285
+ await wait(options, delayMs);
286
+ return false;
287
+ }
288
+ if (options.signal.aborted) {
289
+ return true;
290
+ }
291
+ let abort;
292
+ const aborted = new Promise((resolve) => {
293
+ abort = () => resolve(true);
294
+ options.signal.addEventListener("abort", abort, {once: true});
295
+ });
296
+ try {
297
+ return await Promise.race([Promise.resolve(wait(options, delayMs)).then(() => false), aborted]);
298
+ } finally {
299
+ options.signal.removeEventListener("abort", abort);
300
+ }
301
+ }
302
+
303
+ function invalidSuccessResponse(options) {
304
+ const code = options.endpoint.endsWith("/start") ? "BROWSER_AUTH_START_INVALID" : "BROWSER_AUTH_POLL_INVALID";
305
+ return failure(options.command, cliError(code, {
306
+ path: "data",
307
+ message: "Browser authorization returned malformed JSON.",
308
+ nextActions: ["finebi-cli auth login --browser"],
309
+ }));
310
+ }
311
+
312
+ function clock(options) {
313
+ return options.nowMs ? options.nowMs() : Date.now();
314
+ }
315
+
316
+ function report(options, message) {
317
+ (options.writeStderr || ((text) => process.stderr.write(text)))(`${message}\n`);
318
+ }
319
+
320
+ function openBrowser(url) {
321
+ const command = platform === "win32" ? "rundll32.exe" : platform === "darwin" ? "open" : "xdg-open";
322
+ const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
323
+ return new Promise((resolve, reject) => {
324
+ const child = spawn(command, args, {detached: true, stdio: "ignore", windowsHide: true});
325
+ child.once("error", reject);
326
+ child.once("spawn", () => {
327
+ child.unref();
328
+ resolve();
329
+ });
330
+ });
331
+ }
@@ -0,0 +1,122 @@
1
+ import {existsSync, mkdirSync, readFileSync, writeFileSync} from "node:fs";
2
+ import {dirname, join, win32} from "node:path";
3
+
4
+ import {authenticationFailure} from "./auth-failure.js";
5
+ import {buildAuthHeaders} from "./auth-headers.js";
6
+ import {brandCommandText, brandCommandValue, CLI_API_BASE_PATH} from "./branding.js";
7
+ import {defaultConfigPath} from "./config.js";
8
+ import {cliError} from "./errors.js";
9
+ import {failure, success} from "./output.js";
10
+
11
+ export async function fetchCapabilities(options) {
12
+ const endpoint = `${CLI_API_BASE_PATH}/capabilities`;
13
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
16
+ try {
17
+ const response = await (options.fetchImpl || fetch)(`${baseUrl}${endpoint}`, {
18
+ method: "GET",
19
+ headers: buildAuthHeaders({ token: options.token, accessKey: options.accessKey }),
20
+ signal: controller.signal,
21
+ });
22
+ return await parseCapabilityResponse(response);
23
+ } catch (error) {
24
+ return failure("capability.refresh", cliError("CAPABILITY_REFRESH_FAILED", {
25
+ path: endpoint,
26
+ message: error instanceof Error ? error.message : String(error),
27
+ hint: "Check FineBI base-url and authentication.",
28
+ retryable: true,
29
+ nextActions: ["finebi-cli auth login", "finebi-cli capability refresh"],
30
+ }));
31
+ } finally {
32
+ clearTimeout(timer);
33
+ }
34
+ }
35
+
36
+ export function capabilityCachePath(env = process.env) {
37
+ const configPath = defaultConfigPath(env);
38
+ if (isWindowsPath(configPath)) {
39
+ return win32.join(win32.dirname(configPath), "capabilities.json");
40
+ }
41
+ return join(dirname(configPath), "capabilities.json");
42
+ }
43
+
44
+ export function cacheCapability(path, manifest, options) {
45
+ const fetchedAt = options.fetchedAt;
46
+ const expiresAt = new Date(Date.parse(fetchedAt) + options.ttlMs).toISOString();
47
+ const record = {
48
+ baseUrl: options.baseUrl,
49
+ username: options.username,
50
+ fetchedAt,
51
+ expiresAt,
52
+ ttlMs: options.ttlMs,
53
+ manifest,
54
+ };
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
57
+ return record;
58
+ }
59
+
60
+ export function readCapabilityCache(path, now = new Date().toISOString()) {
61
+ if (!existsSync(path)) {
62
+ return { hit: false, stale: false };
63
+ }
64
+ const record = JSON.parse(readFileSync(path, "utf8"));
65
+ const stale = Date.parse(record.expiresAt) <= Date.parse(now);
66
+ return {
67
+ hit: !stale,
68
+ stale,
69
+ manifest: record.manifest,
70
+ record,
71
+ };
72
+ }
73
+
74
+ export function resolveCapability(cache, purpose) {
75
+ if (cache.hit || (purpose === "help" && cache.stale && cache.manifest)) {
76
+ return { manifest: cache.manifest };
77
+ }
78
+ return {
79
+ error: cliError("CAPABILITY_CACHE_EXPIRED", {
80
+ path: "capabilityCache",
81
+ message: "Capability cache is missing or expired.",
82
+ hint: brandCommandText("Run finebi-cli capability refresh."),
83
+ retryable: true,
84
+ nextActions: brandCommandValue(["finebi-cli capability refresh"]),
85
+ }),
86
+ };
87
+ }
88
+
89
+ async function parseCapabilityResponse(response) {
90
+ const text = await response.text();
91
+ const contentType = response.headers.get("content-type") || "";
92
+ if (!contentType.includes("json") || text.trim().startsWith("<")) {
93
+ return failure("capability.refresh", cliError("AUTH_REDIRECT_OR_HTML_RESPONSE", {
94
+ path: `${CLI_API_BASE_PATH}/capabilities`,
95
+ message: "FineBI returned an HTML page instead of JSON.",
96
+ hint: "Run finebi-cli auth login and retry capability refresh.",
97
+ retryable: true,
98
+ nextActions: ["finebi-cli auth login", "finebi-cli capability refresh"],
99
+ }), {status: response.status});
100
+ }
101
+ if (!response.ok) {
102
+ if (response.status === 401 || response.status === 403) {
103
+ return authenticationFailure("capability.refresh", `${CLI_API_BASE_PATH}/capabilities`, response.status);
104
+ }
105
+ return failure("capability.refresh", cliError("CAPABILITY_REFRESH_FAILED", {
106
+ path: `${CLI_API_BASE_PATH}/capabilities`,
107
+ message: `FineBI returned HTTP ${response.status}: ${text.slice(0, 300)}`,
108
+ hint: "Check FineBI server logs and plugin registration.",
109
+ retryable: true,
110
+ nextActions: ["finebi-cli capability refresh"],
111
+ }), {status: response.status});
112
+ }
113
+ const payload = JSON.parse(text);
114
+ if (payload && payload.ok === false) {
115
+ return payload;
116
+ }
117
+ return success("capability.refresh", payload.data || payload);
118
+ }
119
+
120
+ function isWindowsPath(value) {
121
+ return /^[a-zA-Z]:[\\/]/.test(value) || value.includes("\\");
122
+ }
@@ -0,0 +1,87 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmodSync } from "node:fs";
3
+
4
+ /**
5
+ * 创建配置权限控制器,隐藏 POSIX mode 与 Windows ACL 的平台差异。
6
+ *
7
+ * Windows 的 mode 参数不会收紧 NTFS ACL,因此需要在写入敏感内容前显式关闭继承。
8
+ * 只保留当前用户、SYSTEM 与本机 Administrators 的完全控制权:当前用户可继续使用 CLI,
9
+ * 系统服务和管理员仍能执行备份/恢复,但普通继承主体不能读取 JWT 或 Access Key。
10
+ * 安全命令失败时不阻断配置写入,改为发出明确 warning,避免把登录成功误报成失败。
11
+ */
12
+ export function createConfigPermissionController(options = {}) {
13
+ const platform = options.platform || process.platform;
14
+ if (platform !== "win32") {
15
+ return {
16
+ restrictDirectory: (path) => chmodSync(path, 0o700),
17
+ restrictFile: (path) => chmodSync(path, 0o600),
18
+ };
19
+ }
20
+
21
+ const spawn = options.spawnSyncImpl || spawnSync;
22
+ const warn = options.onPermissionWarning || defaultPermissionWarning;
23
+ const userSid = resolveWindowsUserSid(spawn);
24
+ if (!userSid) {
25
+ warn("Could not resolve the current Windows user SID; FineBI CLI config ACL was not restricted.");
26
+ return noOpPermissionController();
27
+ }
28
+
29
+ return {
30
+ restrictDirectory(path) {
31
+ const acl = [
32
+ "/inheritance:r",
33
+ "/grant:r",
34
+ `*${userSid}:(OI)(CI)F`,
35
+ "*S-1-5-18:(OI)(CI)F",
36
+ "*S-1-5-32-544:(OI)(CI)F",
37
+ ];
38
+ if (!runIcacls(spawn, path, acl)) {
39
+ warn("Could not restrict the FineBI CLI config directory ACL; verify that other users cannot read it.");
40
+ }
41
+ },
42
+ restrictFile(path) {
43
+ const acl = [
44
+ "/inheritance:r",
45
+ "/grant:r",
46
+ `*${userSid}:(F)`,
47
+ "*S-1-5-18:(F)",
48
+ "*S-1-5-32-544:(F)",
49
+ ];
50
+ if (!runIcacls(spawn, path, acl)) {
51
+ warn("Could not restrict the FineBI CLI config file ACL; verify that only the current user can read config.json.");
52
+ }
53
+ },
54
+ };
55
+ }
56
+
57
+ function resolveWindowsUserSid(spawn) {
58
+ const result = spawn("whoami.exe", ["/user", "/fo", "csv", "/nh"], {
59
+ encoding: "utf8",
60
+ windowsHide: true,
61
+ });
62
+ if (result?.error || result?.status !== 0 || typeof result?.stdout !== "string") {
63
+ return undefined;
64
+ }
65
+ return result.stdout.match(/"[^"]*","(S-[^"]+)"/)?.[1];
66
+ }
67
+
68
+ function runIcacls(spawn, path, aclArguments) {
69
+ const result = spawn("icacls.exe", [path, ...aclArguments], {
70
+ encoding: "utf8",
71
+ windowsHide: true,
72
+ });
73
+ return !result?.error && result?.status === 0;
74
+ }
75
+
76
+ function noOpPermissionController() {
77
+ return {
78
+ restrictDirectory() {
79
+ },
80
+ restrictFile() {
81
+ },
82
+ };
83
+ }
84
+
85
+ function defaultPermissionWarning(message) {
86
+ process.emitWarning(message, { code: "FINEBI_CONFIG_PERMISSIONS" });
87
+ }