@burdenoff/bigconsole-cli 2026.1029.1 → 2026.1110.1

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.
@@ -1,151 +0,0 @@
1
- /**
2
- * CLI configuration management.
3
- * Persists CLI-level settings to ~/.config/bigconsole-cli/config.json
4
- */
5
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
6
- import { join } from 'path';
7
- import { homedir } from 'os';
8
- const CONFIG_DIR = join(homedir(), '.config', 'bigconsole-cli');
9
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
10
- /**
11
- * Well-known client ID for the BigConsole CLI.
12
- * This is a system app auto-created by global-auth-svc on startup.
13
- * Same pattern as GitHub CLI (gh), Claude Code, Stripe CLI.
14
- */
15
- export const DEFAULT_CLIENT_ID = 'burdenoff_cli_bigconsole';
16
- /** Production OIDC issuer (global gateway) */
17
- const PROD_OIDC_ISSUER = 'https://graphql.burdenoff.com/global';
18
- /** Production workspace gateway endpoint */
19
- export const PROD_WORKSPACE_ENDPOINT = 'https://graphqlworkspaces.burdenoff.com/workspaces/graphql';
20
- /** Production global gateway endpoint */
21
- export const PROD_GLOBAL_ENDPOINT = 'https://graphql.burdenoff.com/global/graphql';
22
- export const DEFAULT_CONFIG = {
23
- workspaceEndpoint: PROD_WORKSPACE_ENDPOINT,
24
- globalEndpoint: PROD_GLOBAL_ENDPOINT,
25
- };
26
- function ensureConfigDir() {
27
- if (!existsSync(CONFIG_DIR)) {
28
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
29
- }
30
- }
31
- function getEnvOverrides() {
32
- const overrides = {};
33
- if (process.env.BIGCONSOLE_WORKSPACE_ENDPOINT) {
34
- overrides.workspaceEndpoint = process.env.BIGCONSOLE_WORKSPACE_ENDPOINT;
35
- }
36
- if (process.env.BIGCONSOLE_GLOBAL_ENDPOINT) {
37
- overrides.globalEndpoint = process.env.BIGCONSOLE_GLOBAL_ENDPOINT;
38
- }
39
- if (process.env.BIGCONSOLE_OIDC_ISSUER) {
40
- overrides.oidcIssuer = process.env.BIGCONSOLE_OIDC_ISSUER;
41
- }
42
- if (process.env.BIGCONSOLE_CLIENT_ID) {
43
- overrides.clientId = process.env.BIGCONSOLE_CLIENT_ID;
44
- }
45
- return overrides;
46
- }
47
- /**
48
- * Read the CLI configuration, merging with defaults.
49
- *
50
- * Resolution order (later wins):
51
- * 1. Built-in production defaults
52
- * 2. Environment variables BIGCONSOLE_WORKSPACE_ENDPOINT, BIGCONSOLE_GLOBAL_ENDPOINT,
53
- * BIGCONSOLE_OIDC_ISSUER, BIGCONSOLE_CLIENT_ID
54
- * 3. Values stored in ~/.config/bigconsole-cli/config.json
55
- */
56
- export function readConfig() {
57
- try {
58
- const envOverrides = getEnvOverrides();
59
- if (!existsSync(CONFIG_FILE))
60
- return { ...DEFAULT_CONFIG, ...envOverrides };
61
- const raw = readFileSync(CONFIG_FILE, 'utf8');
62
- const stored = JSON.parse(raw);
63
- return { ...DEFAULT_CONFIG, ...envOverrides, ...stored };
64
- }
65
- catch {
66
- return { ...DEFAULT_CONFIG };
67
- }
68
- }
69
- /**
70
- * Write the CLI configuration to disk.
71
- */
72
- export function writeConfig(config) {
73
- ensureConfigDir();
74
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf8' });
75
- try {
76
- chmodSync(CONFIG_FILE, 0o600);
77
- }
78
- catch {
79
- // chmodSync may fail on Windows, non-critical
80
- }
81
- }
82
- /**
83
- * Get a single config value by key.
84
- */
85
- export function getConfigValue(key) {
86
- const config = readConfig();
87
- return config[key];
88
- }
89
- /**
90
- * Set a single config value by key.
91
- */
92
- export function setConfigValue(key, value) {
93
- const config = readConfig();
94
- config[key] = value;
95
- writeConfig(config);
96
- }
97
- /**
98
- * Get the path to the config file.
99
- */
100
- export function getConfigPath() {
101
- return CONFIG_FILE;
102
- }
103
- /**
104
- * Get the config directory path.
105
- */
106
- export function getConfigDir() {
107
- return CONFIG_DIR;
108
- }
109
- /**
110
- * Resolve OIDC configuration with smart defaults.
111
- * - clientId defaults to the built-in BigConsole CLI system app
112
- * - oidcIssuer derives from the configured globalEndpoint
113
- * Users can override both via cli-config if they have a custom OIDC provider.
114
- */
115
- export function resolveOIDCConfig() {
116
- const config = readConfig();
117
- // Client ID: user override > built-in default
118
- const clientId = config.clientId || DEFAULT_CLIENT_ID;
119
- // OIDC issuer: user override > derive from any non-prod endpoint > prod default.
120
- //
121
- // Strip the trailing `/graphql` from the endpoint so OIDC routes (device-code,
122
- // token, etc.) are mounted at the parent path. The second `.replace()` that
123
- // used to target `/global/graphql$` was dead code — the first replace always
124
- // strips the trailing `/graphql` first.
125
- //
126
- // Deriving from the endpoint also covers staging / self-hosted global
127
- // gateways, not just localhost: if the user has explicitly pointed
128
- // `globalEndpoint` somewhere non-prod the issuer should follow, and only the
129
- // implicit prod endpoint falls back to `PROD_OIDC_ISSUER`.
130
- let oidcIssuer = config.oidcIssuer;
131
- if (!oidcIssuer) {
132
- const globalEndpoint = config.globalEndpoint;
133
- const isProdEndpoint = globalEndpoint.includes('graphql.burdenoff.com');
134
- if (isProdEndpoint) {
135
- oidcIssuer = PROD_OIDC_ISSUER;
136
- }
137
- else {
138
- oidcIssuer = globalEndpoint.replace(/\/graphql$/, '');
139
- }
140
- }
141
- return { oidcIssuer, clientId };
142
- }
143
- export const KNOWN_CONFIG_KEYS = [
144
- 'workspaceEndpoint',
145
- 'globalEndpoint',
146
- 'oidcIssuer',
147
- 'clientId',
148
- 'defaultFormat',
149
- 'verbose',
150
- 'authServiceEndpoint',
151
- ];
@@ -1,254 +0,0 @@
1
- /**
2
- * GraphQL client for CLI operations.
3
- * Dual-endpoint: workspace gateway (bigconsole ops) + global gateway (auth ops).
4
- * Auto-injects auth headers, auto-refreshes both platform and workspace tokens.
5
- */
6
- import { GraphQLClient } from 'graphql-request';
7
- import { readConfig } from '../config/index.js';
8
- import { readAuth, writeAuth, isTokenExpiringSoon, isWorkspaceTokenExpiringSoon, } from '../auth/store.js';
9
- import { issueWorkspaceToken, clientCredentialsFlow } from '../auth/device-code.js';
10
- import { printError } from '../utils/output.js';
11
- import { describeRequestError, toSafeErrorMessage, } from '../utils/sanitize.js';
12
- const DEFAULT_TIMEOUT = 30000;
13
- const MAX_RETRIES = 3;
14
- const INITIAL_RETRY_DELAY_MS = 1000;
15
- function sleep(ms) {
16
- return new Promise((resolve) => setTimeout(resolve, ms));
17
- }
18
- function isNetworkOrServerError(error) {
19
- if (error instanceof TypeError)
20
- return true;
21
- if (error && typeof error === 'object' && 'response' in error) {
22
- const resp = error.response;
23
- if (resp && typeof resp.status === 'number' && resp.status >= 500)
24
- return true;
25
- }
26
- return false;
27
- }
28
- /**
29
- * Error thrown for a failed GraphQL request. Its message is built only from
30
- * the HTTP status, the server's first GraphQL error and its error code, so it
31
- * is safe to print. The underlying client error is deliberately not attached
32
- * (no `cause`): it carries the request variables and the raw response body.
33
- */
34
- export class BigConsoleRequestError extends Error {
35
- constructor(description) {
36
- super(description.message);
37
- this.name = 'BigConsoleRequestError';
38
- this.status = description.status;
39
- this.code = description.code;
40
- }
41
- }
42
- let workspaceClient = null;
43
- let globalClient = null;
44
- function getHeaders(auth) {
45
- const headers = {};
46
- if (auth?.accessToken) {
47
- headers['Authorization'] = `Bearer ${auth.accessToken}`;
48
- }
49
- return headers;
50
- }
51
- function getWorkspaceHeaders(auth) {
52
- const headers = getHeaders(auth);
53
- if (auth?.workspaceToken) {
54
- headers['X-Workspace-Authorization'] = `Bearer ${auth.workspaceToken}`;
55
- }
56
- if (auth?.organizationId) {
57
- headers['x-org-id'] = auth.organizationId;
58
- }
59
- return headers;
60
- }
61
- function initClients() {
62
- const config = readConfig();
63
- const auth = readAuth();
64
- workspaceClient = new GraphQLClient(config.workspaceEndpoint);
65
- workspaceClient.setHeaders(getWorkspaceHeaders(auth));
66
- globalClient = new GraphQLClient(config.globalEndpoint);
67
- globalClient.setHeaders(getHeaders(auth));
68
- return { workspace: workspaceClient, global: globalClient };
69
- }
70
- /**
71
- * Refresh the platform access token.
72
- * For client_credentials: re-authenticate with stored credentials.
73
- * For device_code/api_key: use the stored refresh token.
74
- */
75
- async function refreshPlatformToken() {
76
- const auth = readAuth();
77
- if (!auth)
78
- return;
79
- if (auth.authMethod === 'client_credentials' && auth.clientId && auth.clientSecret) {
80
- const config = readConfig();
81
- try {
82
- const result = await clientCredentialsFlow(config.globalEndpoint, auth.clientId, auth.clientSecret);
83
- writeAuth({
84
- ...auth,
85
- accessToken: result.accessToken,
86
- expiresAt: Date.now() + result.expiresIn * 1000,
87
- });
88
- workspaceClient = null;
89
- globalClient = null;
90
- }
91
- catch (error) {
92
- printError(`App token refresh failed: ${toSafeErrorMessage(error)}`);
93
- }
94
- return;
95
- }
96
- if (!auth.refreshToken)
97
- return;
98
- const config = readConfig();
99
- const client = new GraphQLClient(config.globalEndpoint);
100
- client.setHeaders({ Authorization: `Bearer ${auth.accessToken}` });
101
- const REFRESH_MUTATION = `
102
- mutation RefreshToken($token: String!) {
103
- refreshToken(token: $token) {
104
- accessToken
105
- refreshToken
106
- expiresIn
107
- }
108
- }
109
- `;
110
- try {
111
- const data = await client.request(REFRESH_MUTATION, {
112
- token: auth.refreshToken,
113
- });
114
- const result = data.refreshToken;
115
- const expiresAt = Date.now() + result.expiresIn * 1000;
116
- writeAuth({
117
- ...auth,
118
- accessToken: result.accessToken,
119
- refreshToken: result.refreshToken,
120
- expiresAt,
121
- });
122
- workspaceClient = null;
123
- globalClient = null;
124
- }
125
- catch (error) {
126
- printError(`Token refresh failed: ${toSafeErrorMessage(error)}`);
127
- }
128
- }
129
- /**
130
- * Refresh the workspace token using the current platform token.
131
- */
132
- async function refreshWorkspaceToken() {
133
- const auth = readAuth();
134
- if (!auth?.accessToken || !auth.workspaceId || !auth.organizationId)
135
- return;
136
- const config = readConfig();
137
- try {
138
- const result = await issueWorkspaceToken(config.workspaceEndpoint, auth.accessToken, auth.workspaceId, auth.organizationId);
139
- writeAuth({
140
- ...auth,
141
- workspaceToken: result.token,
142
- workspaceTokenExpiresAt: new Date(result.expiresAt).getTime(),
143
- });
144
- workspaceClient = null;
145
- }
146
- catch (error) {
147
- printError(`Workspace token refresh failed: ${toSafeErrorMessage(error)}`);
148
- }
149
- }
150
- export async function ensureFreshTokens() {
151
- const auth = readAuth();
152
- if (!auth)
153
- return;
154
- if (isTokenExpiringSoon(auth)) {
155
- await refreshPlatformToken();
156
- }
157
- const freshAuth = readAuth();
158
- if (freshAuth && freshAuth.workspaceToken && isWorkspaceTokenExpiringSoon(freshAuth)) {
159
- await refreshWorkspaceToken();
160
- }
161
- }
162
- /**
163
- * Inspect a GraphQL document to decide whether it is safe to retry.
164
- * Mutations are NOT retried — a transient error may land the write
165
- * server-side even if the client observed a timeout, so blindly
166
- * retrying create/update/delete could create duplicates. Queries are
167
- * always retryable.
168
- */
169
- function isRetryableDocument(document) {
170
- const text = typeof document === 'string'
171
- ? document
172
- : typeof document?.loc?.source?.body ===
173
- 'string'
174
- ? document.loc.source.body
175
- : '';
176
- const m = text.match(/\b(query|mutation|subscription)\b/i);
177
- return !m || m[1].toLowerCase() === 'query';
178
- }
179
- async function executeWithRetry(client, document, variables) {
180
- const retryable = isRetryableDocument(document);
181
- const maxAttempts = retryable ? MAX_RETRIES : 1;
182
- let lastError;
183
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
184
- try {
185
- const controller = new AbortController();
186
- const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
187
- try {
188
- return await client.request({
189
- document,
190
- variables,
191
- signal: controller.signal,
192
- });
193
- }
194
- finally {
195
- clearTimeout(timer);
196
- }
197
- }
198
- catch (error) {
199
- lastError = error;
200
- if (error && typeof error === 'object' && 'response' in error) {
201
- const resp = error.response;
202
- if (resp && typeof resp.status === 'number' && resp.status >= 400 && resp.status < 500) {
203
- break;
204
- }
205
- }
206
- if (retryable && isNetworkOrServerError(error) && attempt < maxAttempts - 1) {
207
- const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
208
- await sleep(delay);
209
- continue;
210
- }
211
- break;
212
- }
213
- }
214
- throw new BigConsoleRequestError(describeRequestError(lastError));
215
- }
216
- /**
217
- * Execute a GraphQL request against the workspace gateway.
218
- * Auto-refreshes both platform and workspace tokens when they are about to expire.
219
- */
220
- export async function workspaceRequest(document, variables) {
221
- await ensureFreshTokens();
222
- const { workspace } = workspaceClient ? { workspace: workspaceClient } : initClients();
223
- const auth = readAuth();
224
- workspace.setHeaders(getWorkspaceHeaders(auth));
225
- return executeWithRetry(workspace, document, variables);
226
- }
227
- /**
228
- * Execute a GraphQL request against the global gateway (auth operations).
229
- * Does NOT auto-refresh tokens (to prevent loops during refresh itself).
230
- */
231
- export async function globalRequest(document, variables) {
232
- const { global: client } = globalClient ? { global: globalClient } : initClients();
233
- const auth = readAuth();
234
- client.setHeaders(getHeaders(auth));
235
- return executeWithRetry(client, document, variables);
236
- }
237
- /**
238
- * Execute a request against the workspace gateway with ONLY the platform auth token.
239
- * Used for IssueWorkspaceToken which does not require an existing workspace token.
240
- */
241
- export async function workspaceAuthOnlyRequest(document, variables) {
242
- const config = readConfig();
243
- const auth = readAuth();
244
- const client = new GraphQLClient(config.workspaceEndpoint);
245
- client.setHeaders(getHeaders(auth));
246
- return executeWithRetry(client, document, variables);
247
- }
248
- /**
249
- * Reset cached clients (useful after auth changes).
250
- */
251
- export function resetClients() {
252
- workspaceClient = null;
253
- globalClient = null;
254
- }