@girardmedia/bootspring 2.2.0 → 2.3.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 +2 -2
- package/bin/bootspring.js +35 -96
- package/claude-commands/agent.md +34 -0
- package/claude-commands/bs.md +31 -0
- package/claude-commands/build.md +25 -0
- package/claude-commands/skill.md +31 -0
- package/claude-commands/todo.md +25 -0
- package/dist/cli/index.cjs +17808 -0
- package/dist/core/index.d.ts +5814 -0
- package/dist/core.js +5780 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp-server.js +2299 -0
- package/generators/api-docs.js +2 -2
- package/generators/decisions.js +3 -3
- package/generators/health.js +16 -16
- package/generators/sprint.js +2 -2
- package/package.json +27 -59
- package/core/api-client.d.ts +0 -69
- package/core/api-client.js +0 -1482
- package/core/auth.d.ts +0 -98
- package/core/auth.js +0 -737
- package/core/build-orchestrator.js +0 -508
- package/core/build-state.js +0 -612
- package/core/config.d.ts +0 -106
- package/core/config.js +0 -1328
- package/core/context-loader.js +0 -580
- package/core/context.d.ts +0 -61
- package/core/context.js +0 -327
- package/core/entitlements.d.ts +0 -70
- package/core/entitlements.js +0 -322
- package/core/index.d.ts +0 -53
- package/core/index.js +0 -62
- package/core/mcp-config.js +0 -115
- package/core/policies.d.ts +0 -43
- package/core/policies.js +0 -113
- package/core/policy-matrix.js +0 -303
- package/core/project-activity.js +0 -175
- package/core/redaction.d.ts +0 -5
- package/core/redaction.js +0 -63
- package/core/self-update.js +0 -259
- package/core/session.js +0 -353
- package/core/task-extractor.js +0 -1098
- package/core/telemetry.d.ts +0 -55
- package/core/telemetry.js +0 -617
- package/core/tier-enforcement.js +0 -928
- package/core/utils.d.ts +0 -90
- package/core/utils.js +0 -455
- package/core/validation.js +0 -572
- package/mcp/server.d.ts +0 -57
- package/mcp/server.js +0 -264
package/core/api-client.js
DELETED
|
@@ -1,1482 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring API Client
|
|
3
|
-
*
|
|
4
|
-
* Handles all communication with api.bootspring.com
|
|
5
|
-
* This is the thin-client version that requires API for all operations.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const https = require('https');
|
|
9
|
-
const http = require('http');
|
|
10
|
-
const auth = require('./auth');
|
|
11
|
-
const session = require('./session');
|
|
12
|
-
const { redactSensitiveString, redactSensitiveData } = require('./redaction');
|
|
13
|
-
|
|
14
|
-
const API_BASE = process.env.BOOTSPRING_API_URL || 'https://api.bootspring.com';
|
|
15
|
-
const API_VERSION = 'v1'; // Note: auth routes don't use version prefix
|
|
16
|
-
|
|
17
|
-
// Cache for API responses
|
|
18
|
-
const cache = new Map();
|
|
19
|
-
const CACHE_TTL = 60000; // 1 minute
|
|
20
|
-
const PRESEED_CODEBASE_ENRICHMENT_PATHS = [
|
|
21
|
-
'/preseed/enrich/from-codebase',
|
|
22
|
-
'/preseed/from-codebase/enrich',
|
|
23
|
-
'/projects/preseed/enrich/from-codebase'
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
function getPackageVersion() {
|
|
27
|
-
try {
|
|
28
|
-
return require('../package.json').version;
|
|
29
|
-
} catch {
|
|
30
|
-
return '0.0.0';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function formatHttpErrorBody(body, statusCode) {
|
|
35
|
-
const raw = String(body || '').trim();
|
|
36
|
-
if (!raw) {
|
|
37
|
-
return `API Error (${statusCode || 'unknown'})`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (/^\s*<!doctype html/i.test(raw) || /^\s*<html/i.test(raw)) {
|
|
41
|
-
const titleMatch = raw.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
42
|
-
const title = titleMatch && titleMatch[1] ? `: ${titleMatch[1].trim()}` : '';
|
|
43
|
-
return `Bootspring API returned an HTML error page (HTTP ${statusCode || 'unknown'}${title})`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return redactSensitiveString(raw);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseExpiresInSeconds(rawValue) {
|
|
50
|
-
if (typeof rawValue === 'number' && Number.isFinite(rawValue) && rawValue > 0) {
|
|
51
|
-
return rawValue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (typeof rawValue === 'string' && rawValue.trim().length > 0) {
|
|
55
|
-
const trimmed = rawValue.trim();
|
|
56
|
-
const asNumber = Number(trimmed);
|
|
57
|
-
if (Number.isFinite(asNumber) && asNumber > 0) {
|
|
58
|
-
return asNumber;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const match = trimmed.match(/^(\d+)([smhd])$/i);
|
|
62
|
-
if (match && match[1] && match[2]) {
|
|
63
|
-
const value = Number(match[1]);
|
|
64
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
65
|
-
return 15 * 60;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const unit = match[2].toLowerCase();
|
|
69
|
-
if (unit === 's') return value;
|
|
70
|
-
if (unit === 'm') return value * 60;
|
|
71
|
-
if (unit === 'h') return value * 60 * 60;
|
|
72
|
-
if (unit === 'd') return value * 24 * 60 * 60;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return 15 * 60;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseProjectScopedTokenPayload(raw) {
|
|
80
|
-
const payload = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
81
|
-
? raw
|
|
82
|
-
: null;
|
|
83
|
-
|
|
84
|
-
if (!payload) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const nestedData = payload.data;
|
|
89
|
-
const source = (nestedData && typeof nestedData === 'object' && !Array.isArray(nestedData))
|
|
90
|
-
? nestedData
|
|
91
|
-
: payload;
|
|
92
|
-
|
|
93
|
-
const tokenCandidates = [
|
|
94
|
-
source.token,
|
|
95
|
-
source.sessionToken,
|
|
96
|
-
source.session_token,
|
|
97
|
-
source.accessToken,
|
|
98
|
-
source.access_token
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const token = tokenCandidates.find(value => typeof value === 'string' && value.length > 0);
|
|
102
|
-
if (!token) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const explicitExpiresAt = [source.expiresAt, source.expires_at]
|
|
107
|
-
.find(value => typeof value === 'string' && value.length > 0);
|
|
108
|
-
|
|
109
|
-
const expiresInSeconds = parseExpiresInSeconds(
|
|
110
|
-
[source.expiresIn, source.expires_in, source.ttl, source.ttlSeconds].find(value => value !== undefined)
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const expiresAt = explicitExpiresAt || new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
114
|
-
return {
|
|
115
|
-
token,
|
|
116
|
-
expiresAt,
|
|
117
|
-
expiresInSeconds
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function requestProjectScopedToken(pathname, apiKey, projectId) {
|
|
122
|
-
const url = new URL(pathname, API_BASE);
|
|
123
|
-
const isHttps = url.protocol === 'https:';
|
|
124
|
-
const httpModule = isHttps ? https : http;
|
|
125
|
-
const deviceContext = auth.getDeviceContext();
|
|
126
|
-
|
|
127
|
-
return new Promise((resolve, reject) => {
|
|
128
|
-
const requestBody = JSON.stringify({
|
|
129
|
-
apiKey,
|
|
130
|
-
projectId,
|
|
131
|
-
scope: 'project',
|
|
132
|
-
deviceFingerprint: deviceContext.deviceId,
|
|
133
|
-
deviceName: `CLI - ${deviceContext.hostname}`
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const req = httpModule.request(url, {
|
|
137
|
-
method: 'POST',
|
|
138
|
-
headers: {
|
|
139
|
-
'Content-Type': 'application/json',
|
|
140
|
-
'User-Agent': `bootspring-cli/${getPackageVersion()}`,
|
|
141
|
-
'Content-Length': Buffer.byteLength(requestBody).toString()
|
|
142
|
-
},
|
|
143
|
-
timeout: 10000
|
|
144
|
-
}, (res) => {
|
|
145
|
-
let body = '';
|
|
146
|
-
res.on('data', chunk => body += chunk);
|
|
147
|
-
res.on('end', () => {
|
|
148
|
-
let parsed = null;
|
|
149
|
-
try {
|
|
150
|
-
parsed = body ? JSON.parse(body) : {};
|
|
151
|
-
} catch {
|
|
152
|
-
parsed = {};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (res.statusCode >= 400) {
|
|
156
|
-
const payload = parsed && typeof parsed === 'object' ? parsed : {};
|
|
157
|
-
const error = new Error(
|
|
158
|
-
redactSensitiveString(String(payload.message || payload.error || `Token exchange failed (${res.statusCode})`))
|
|
159
|
-
);
|
|
160
|
-
error.status = res.statusCode;
|
|
161
|
-
error.code = typeof payload.code === 'string' ? payload.code : undefined;
|
|
162
|
-
error.details = redactSensitiveData(payload.details);
|
|
163
|
-
reject(error);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const exchange = parseProjectScopedTokenPayload(parsed);
|
|
168
|
-
if (!exchange) {
|
|
169
|
-
const error = new Error('Token exchange response did not include a scoped token');
|
|
170
|
-
error.status = res.statusCode;
|
|
171
|
-
reject(error);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
resolve(exchange);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
req.on('error', (error) => {
|
|
180
|
-
reject(new Error(redactSensitiveString(error.message || String(error))));
|
|
181
|
-
});
|
|
182
|
-
req.on('timeout', () => {
|
|
183
|
-
req.destroy();
|
|
184
|
-
reject(new Error('Token exchange request timeout'));
|
|
185
|
-
});
|
|
186
|
-
req.write(requestBody);
|
|
187
|
-
req.end();
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function exchangeProjectScopedToken(apiKey, projectId = null) {
|
|
192
|
-
const paths = [
|
|
193
|
-
'/api/keys/scoped-token',
|
|
194
|
-
'/api/keys/exchange',
|
|
195
|
-
'/api/keys/session'
|
|
196
|
-
];
|
|
197
|
-
|
|
198
|
-
let lastError = null;
|
|
199
|
-
|
|
200
|
-
for (const exchangePath of paths) {
|
|
201
|
-
try {
|
|
202
|
-
const exchange = await requestProjectScopedToken(exchangePath, apiKey, projectId);
|
|
203
|
-
return {
|
|
204
|
-
...exchange,
|
|
205
|
-
sourcePath: exchangePath
|
|
206
|
-
};
|
|
207
|
-
} catch (error) {
|
|
208
|
-
lastError = error;
|
|
209
|
-
if (error.status && error.status >= 400 && error.status < 500 &&
|
|
210
|
-
error.status !== 404 && error.status !== 405) {
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
throw lastError || new Error('Failed to exchange scoped token');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function ensureProjectScopedToken() {
|
|
220
|
-
if (process.env.BOOTSPRING_API_KEY) {
|
|
221
|
-
return process.env.BOOTSPRING_API_KEY;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const scopedToken = auth.getProjectScopedToken ? auth.getProjectScopedToken() : null;
|
|
225
|
-
if (scopedToken) {
|
|
226
|
-
return scopedToken;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const project = session.getEffectiveProject ? session.getEffectiveProject() : null;
|
|
230
|
-
const projectId = project?.id || null;
|
|
231
|
-
|
|
232
|
-
const storedApiKey = auth.getStoredApiKey ? auth.getStoredApiKey() : null;
|
|
233
|
-
if (storedApiKey) {
|
|
234
|
-
try {
|
|
235
|
-
const exchanged = await exchangeProjectScopedToken(storedApiKey, projectId);
|
|
236
|
-
if (auth.saveProjectScopedSession) {
|
|
237
|
-
auth.saveProjectScopedSession(exchanged.token, {
|
|
238
|
-
expiresAt: exchanged.expiresAt,
|
|
239
|
-
source: exchanged.sourcePath
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
return exchanged.token;
|
|
243
|
-
} catch {
|
|
244
|
-
return storedApiKey;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const legacyProjectApiKey = auth.getLegacyProjectApiKey ? auth.getLegacyProjectApiKey() : null;
|
|
249
|
-
if (legacyProjectApiKey) {
|
|
250
|
-
try {
|
|
251
|
-
const exchanged = await exchangeProjectScopedToken(legacyProjectApiKey, projectId);
|
|
252
|
-
if (auth.saveProjectScopedSession) {
|
|
253
|
-
auth.saveProjectScopedSession(exchanged.token, {
|
|
254
|
-
expiresAt: exchanged.expiresAt,
|
|
255
|
-
source: exchanged.sourcePath,
|
|
256
|
-
migratedFromLegacyApiKey: true
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
if (auth.clearProjectApiKey) {
|
|
260
|
-
auth.clearProjectApiKey();
|
|
261
|
-
}
|
|
262
|
-
return exchanged.token;
|
|
263
|
-
} catch {
|
|
264
|
-
return legacyProjectApiKey;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Proactively refresh token if expiring soon
|
|
273
|
-
* @returns {Promise<boolean>} True if refresh was performed
|
|
274
|
-
*/
|
|
275
|
-
async function proactiveTokenRefresh() {
|
|
276
|
-
const expiryStatus = auth.getTokenExpiryStatus ? auth.getTokenExpiryStatus(5 * 60 * 1000) : null;
|
|
277
|
-
|
|
278
|
-
if (!expiryStatus) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// If token is expiring within 5 minutes but not yet expired, refresh it
|
|
283
|
-
if (expiryStatus.expiringSoon && !expiryStatus.expired) {
|
|
284
|
-
const refreshToken = auth.getRefreshToken();
|
|
285
|
-
if (refreshToken) {
|
|
286
|
-
try {
|
|
287
|
-
const deviceContext = auth.getDeviceContext();
|
|
288
|
-
const url = new URL('/api/v1/auth/refresh', API_BASE);
|
|
289
|
-
const isHttps = url.protocol === 'https:';
|
|
290
|
-
const httpModule = isHttps ? https : http;
|
|
291
|
-
|
|
292
|
-
const response = await new Promise((resolve, reject) => {
|
|
293
|
-
const requestBody = JSON.stringify({ refreshToken, device: deviceContext });
|
|
294
|
-
const req = httpModule.request(url, {
|
|
295
|
-
method: 'POST',
|
|
296
|
-
headers: {
|
|
297
|
-
'Content-Type': 'application/json',
|
|
298
|
-
'User-Agent': `bootspring-cli/${getPackageVersion()}`,
|
|
299
|
-
'Content-Length': Buffer.byteLength(requestBody)
|
|
300
|
-
},
|
|
301
|
-
timeout: 10000
|
|
302
|
-
}, (res) => {
|
|
303
|
-
let body = '';
|
|
304
|
-
res.on('data', chunk => body += chunk);
|
|
305
|
-
res.on('end', () => {
|
|
306
|
-
try {
|
|
307
|
-
const json = JSON.parse(body);
|
|
308
|
-
if (res.statusCode >= 400) {
|
|
309
|
-
reject(new Error(json.message || 'Token refresh failed'));
|
|
310
|
-
} else {
|
|
311
|
-
resolve(json);
|
|
312
|
-
}
|
|
313
|
-
} catch {
|
|
314
|
-
reject(new Error('Invalid refresh response'));
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
req.on('error', reject);
|
|
319
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
320
|
-
req.write(requestBody);
|
|
321
|
-
req.end();
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
auth.updateTokens(response);
|
|
325
|
-
return true;
|
|
326
|
-
} catch {
|
|
327
|
-
// Refresh failed - continue with existing token or fall back
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function resolveAuthHeaders() {
|
|
337
|
-
// Try to proactively refresh token if expiring soon
|
|
338
|
-
await proactiveTokenRefresh();
|
|
339
|
-
|
|
340
|
-
const token = auth.getToken();
|
|
341
|
-
if (token) {
|
|
342
|
-
return { Authorization: `Bearer ${token}` };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const scopedOrApiKey = await ensureProjectScopedToken();
|
|
346
|
-
if (scopedOrApiKey) {
|
|
347
|
-
return { 'X-API-Key': scopedOrApiKey };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const fallbackApiKey = auth.getApiKey();
|
|
351
|
-
if (fallbackApiKey) {
|
|
352
|
-
return { 'X-API-Key': fallbackApiKey };
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return {};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Make an API request (with version prefix)
|
|
360
|
-
*/
|
|
361
|
-
async function request(method, path, data = null, options = {}) {
|
|
362
|
-
const authHeaders = await resolveAuthHeaders();
|
|
363
|
-
const url = new URL(`/api/${API_VERSION}${path}`, API_BASE);
|
|
364
|
-
const isHttps = url.protocol === 'https:';
|
|
365
|
-
const httpModule = isHttps ? https : http;
|
|
366
|
-
|
|
367
|
-
// Get device ID for request tracking
|
|
368
|
-
const deviceId = auth.getDeviceId();
|
|
369
|
-
|
|
370
|
-
// Get project context for tracking
|
|
371
|
-
const project = session.getEffectiveProject();
|
|
372
|
-
const projectId = project?.id || null;
|
|
373
|
-
|
|
374
|
-
const headers = {
|
|
375
|
-
'Content-Type': 'application/json',
|
|
376
|
-
'User-Agent': `bootspring-cli/${getPackageVersion()}`,
|
|
377
|
-
'X-Device-Id': deviceId,
|
|
378
|
-
...(projectId && { 'X-Project-Id': projectId }),
|
|
379
|
-
...authHeaders,
|
|
380
|
-
...options.headers
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// Check cache for GET requests
|
|
384
|
-
if (method === 'GET' && !options.noCache) {
|
|
385
|
-
const cacheKey = `${method}:${path}`;
|
|
386
|
-
const cached = cache.get(cacheKey);
|
|
387
|
-
if (cached && Date.now() - cached.time < CACHE_TTL) {
|
|
388
|
-
return cached.data;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
const req = httpModule.request(url, {
|
|
394
|
-
method,
|
|
395
|
-
headers,
|
|
396
|
-
timeout: options.timeout || 30000
|
|
397
|
-
}, (res) => {
|
|
398
|
-
let body = '';
|
|
399
|
-
res.on('data', chunk => body += chunk);
|
|
400
|
-
res.on('end', () => {
|
|
401
|
-
try {
|
|
402
|
-
const json = JSON.parse(body);
|
|
403
|
-
|
|
404
|
-
if (res.statusCode >= 400) {
|
|
405
|
-
const error = new Error(
|
|
406
|
-
redactSensitiveString(String(json.message || json.error || 'API Error'))
|
|
407
|
-
);
|
|
408
|
-
error.status = res.statusCode;
|
|
409
|
-
error.code = json.error || json.code;
|
|
410
|
-
error.details = redactSensitiveData(json.details);
|
|
411
|
-
reject(error);
|
|
412
|
-
} else {
|
|
413
|
-
// Cache successful GET responses
|
|
414
|
-
if (method === 'GET' && !options.noCache) {
|
|
415
|
-
const cacheKey = `${method}:${path}`;
|
|
416
|
-
cache.set(cacheKey, { data: json, time: Date.now() });
|
|
417
|
-
}
|
|
418
|
-
resolve(json);
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
if (res.statusCode >= 400) {
|
|
422
|
-
const error = new Error(formatHttpErrorBody(body, res.statusCode));
|
|
423
|
-
error.status = res.statusCode;
|
|
424
|
-
reject(error);
|
|
425
|
-
} else {
|
|
426
|
-
resolve(body);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
req.on('error', (err) => {
|
|
433
|
-
if (err.code === 'ECONNREFUSED') {
|
|
434
|
-
reject(new Error('Cannot connect to Bootspring API. Please check your internet connection.'));
|
|
435
|
-
} else {
|
|
436
|
-
reject(new Error(redactSensitiveString(err.message || String(err))));
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
req.on('timeout', () => {
|
|
441
|
-
req.destroy();
|
|
442
|
-
reject(new Error('Request timeout'));
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
if (data) {
|
|
446
|
-
req.write(JSON.stringify(data));
|
|
447
|
-
}
|
|
448
|
-
req.end();
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Make a direct API request (without version prefix, for /api/projects etc.)
|
|
454
|
-
*/
|
|
455
|
-
async function directRequest(method, path, data = null, options = {}) {
|
|
456
|
-
const authHeaders = await resolveAuthHeaders();
|
|
457
|
-
const url = new URL(`/api${path}`, API_BASE);
|
|
458
|
-
const isHttps = url.protocol === 'https:';
|
|
459
|
-
const httpModule = isHttps ? https : http;
|
|
460
|
-
|
|
461
|
-
const deviceId = auth.getDeviceId();
|
|
462
|
-
const project = session.getEffectiveProject();
|
|
463
|
-
const projectId = project?.id || null;
|
|
464
|
-
|
|
465
|
-
const headers = {
|
|
466
|
-
'Content-Type': 'application/json',
|
|
467
|
-
'User-Agent': `bootspring-cli/${getPackageVersion()}`,
|
|
468
|
-
'X-Device-Id': deviceId,
|
|
469
|
-
...(projectId && { 'X-Project-Id': projectId }),
|
|
470
|
-
...authHeaders,
|
|
471
|
-
...options.headers
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
return new Promise((resolve, reject) => {
|
|
475
|
-
const req = httpModule.request(url, {
|
|
476
|
-
method,
|
|
477
|
-
headers,
|
|
478
|
-
timeout: options.timeout || 30000
|
|
479
|
-
}, (res) => {
|
|
480
|
-
let body = '';
|
|
481
|
-
res.on('data', chunk => body += chunk);
|
|
482
|
-
res.on('end', () => {
|
|
483
|
-
try {
|
|
484
|
-
const json = JSON.parse(body);
|
|
485
|
-
if (res.statusCode >= 400) {
|
|
486
|
-
const error = new Error(
|
|
487
|
-
redactSensitiveString(String(json.message || json.error || 'API Error'))
|
|
488
|
-
);
|
|
489
|
-
error.status = res.statusCode;
|
|
490
|
-
error.code = json.error || json.code;
|
|
491
|
-
error.details = redactSensitiveData(json.details);
|
|
492
|
-
reject(error);
|
|
493
|
-
} else {
|
|
494
|
-
resolve(json);
|
|
495
|
-
}
|
|
496
|
-
} catch {
|
|
497
|
-
if (res.statusCode >= 400) {
|
|
498
|
-
const error = new Error(formatHttpErrorBody(body, res.statusCode));
|
|
499
|
-
error.status = res.statusCode;
|
|
500
|
-
reject(error);
|
|
501
|
-
} else {
|
|
502
|
-
resolve(body);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
req.on('error', (err) => {
|
|
509
|
-
if (err.code === 'ECONNREFUSED') {
|
|
510
|
-
reject(new Error('Cannot connect to Bootspring API. Please check your internet connection.'));
|
|
511
|
-
} else {
|
|
512
|
-
reject(new Error(redactSensitiveString(err.message || String(err))));
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
req.on('timeout', () => {
|
|
517
|
-
req.destroy();
|
|
518
|
-
reject(new Error('Request timeout'));
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
if (data) {
|
|
522
|
-
req.write(JSON.stringify(data));
|
|
523
|
-
}
|
|
524
|
-
req.end();
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Clear the cache
|
|
530
|
-
*/
|
|
531
|
-
function clearCache() {
|
|
532
|
-
cache.clear();
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Check if API is reachable
|
|
537
|
-
*/
|
|
538
|
-
async function healthCheck() {
|
|
539
|
-
try {
|
|
540
|
-
// Health endpoint is at /health, not under /api/v1
|
|
541
|
-
const url = new URL('/health', API_BASE);
|
|
542
|
-
const isHttps = url.protocol === 'https:';
|
|
543
|
-
const httpModule = isHttps ? https : http;
|
|
544
|
-
|
|
545
|
-
return new Promise((resolve) => {
|
|
546
|
-
const req = httpModule.request(url, {
|
|
547
|
-
method: 'GET',
|
|
548
|
-
timeout: 5000
|
|
549
|
-
}, (res) => {
|
|
550
|
-
let body = '';
|
|
551
|
-
res.on('data', chunk => body += chunk);
|
|
552
|
-
res.on('end', async () => {
|
|
553
|
-
try {
|
|
554
|
-
const json = JSON.parse(body);
|
|
555
|
-
resolve({ connected: true, version: json.version });
|
|
556
|
-
} catch {
|
|
557
|
-
resolve({ connected: false, error: 'Invalid response' });
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
req.on('error', (err) => {
|
|
563
|
-
resolve({ connected: false, error: redactSensitiveString(err.message) });
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
req.on('timeout', () => {
|
|
567
|
-
req.destroy();
|
|
568
|
-
resolve({ connected: false, error: 'Timeout' });
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
req.end();
|
|
572
|
-
});
|
|
573
|
-
} catch (error) {
|
|
574
|
-
return { connected: false, error: redactSensitiveString(error?.message || String(error)) };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Require authentication - throws if not logged in
|
|
580
|
-
*/
|
|
581
|
-
function requireAuth() {
|
|
582
|
-
if (!auth.isAuthenticated()) {
|
|
583
|
-
const error = new Error('Authentication required. Run: bootspring auth login');
|
|
584
|
-
error.code = 'AUTH_REQUIRED';
|
|
585
|
-
throw error;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* API methods
|
|
591
|
-
*/
|
|
592
|
-
const api = {
|
|
593
|
-
// Device Authorization Flow
|
|
594
|
-
async requestDeviceCode() {
|
|
595
|
-
const deviceContext = auth.getDeviceContext();
|
|
596
|
-
const url = new URL('/api/v1/auth/device', API_BASE);
|
|
597
|
-
const isHttps = url.protocol === 'https:';
|
|
598
|
-
const httpModule = isHttps ? https : http;
|
|
599
|
-
|
|
600
|
-
return new Promise((resolve, reject) => {
|
|
601
|
-
const req = httpModule.request(url, {
|
|
602
|
-
method: 'POST',
|
|
603
|
-
headers: {
|
|
604
|
-
'Content-Type': 'application/json',
|
|
605
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
606
|
-
},
|
|
607
|
-
timeout: 10000
|
|
608
|
-
}, (res) => {
|
|
609
|
-
let body = '';
|
|
610
|
-
res.on('data', chunk => body += chunk);
|
|
611
|
-
res.on('end', async () => {
|
|
612
|
-
try {
|
|
613
|
-
const json = JSON.parse(body);
|
|
614
|
-
if (res.statusCode >= 400) {
|
|
615
|
-
const error = new Error(
|
|
616
|
-
redactSensitiveString(String(json.message || json.error || 'Failed to get device code'))
|
|
617
|
-
);
|
|
618
|
-
error.status = res.statusCode;
|
|
619
|
-
error.code = json.error;
|
|
620
|
-
reject(error);
|
|
621
|
-
} else {
|
|
622
|
-
resolve(json);
|
|
623
|
-
}
|
|
624
|
-
} catch {
|
|
625
|
-
reject(new Error('Invalid response from API'));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
630
|
-
req.on('timeout', () => {
|
|
631
|
-
req.destroy();
|
|
632
|
-
reject(new Error('Request timeout'));
|
|
633
|
-
});
|
|
634
|
-
req.write(JSON.stringify({ device: deviceContext }));
|
|
635
|
-
req.end();
|
|
636
|
-
});
|
|
637
|
-
},
|
|
638
|
-
|
|
639
|
-
async pollDeviceToken(deviceCode) {
|
|
640
|
-
const url = new URL('/api/v1/auth/device/token', API_BASE);
|
|
641
|
-
const isHttps = url.protocol === 'https:';
|
|
642
|
-
const httpModule = isHttps ? https : http;
|
|
643
|
-
|
|
644
|
-
return new Promise((resolve, reject) => {
|
|
645
|
-
const req = httpModule.request(url, {
|
|
646
|
-
method: 'POST',
|
|
647
|
-
headers: {
|
|
648
|
-
'Content-Type': 'application/json',
|
|
649
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
650
|
-
},
|
|
651
|
-
timeout: 10000
|
|
652
|
-
}, (res) => {
|
|
653
|
-
let body = '';
|
|
654
|
-
res.on('data', chunk => body += chunk);
|
|
655
|
-
res.on('end', () => {
|
|
656
|
-
try {
|
|
657
|
-
const json = JSON.parse(body);
|
|
658
|
-
if (res.statusCode >= 400) {
|
|
659
|
-
const error = new Error(
|
|
660
|
-
redactSensitiveString(String(json.message || json.error || 'Authorization pending'))
|
|
661
|
-
);
|
|
662
|
-
error.status = res.statusCode;
|
|
663
|
-
error.code = json.error;
|
|
664
|
-
error.details = redactSensitiveData(json);
|
|
665
|
-
reject(error);
|
|
666
|
-
} else {
|
|
667
|
-
resolve(json);
|
|
668
|
-
}
|
|
669
|
-
} catch {
|
|
670
|
-
reject(new Error('Invalid response from API'));
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
675
|
-
req.on('timeout', () => {
|
|
676
|
-
req.destroy();
|
|
677
|
-
reject(new Error('Request timeout'));
|
|
678
|
-
});
|
|
679
|
-
req.write(JSON.stringify({ device_code: deviceCode }));
|
|
680
|
-
req.end();
|
|
681
|
-
});
|
|
682
|
-
},
|
|
683
|
-
|
|
684
|
-
// Auth
|
|
685
|
-
async login(email, password) {
|
|
686
|
-
const deviceContext = auth.getDeviceContext();
|
|
687
|
-
const response = await request('POST', '/auth/login', {
|
|
688
|
-
email,
|
|
689
|
-
password,
|
|
690
|
-
device: deviceContext
|
|
691
|
-
});
|
|
692
|
-
auth.login(response);
|
|
693
|
-
return response;
|
|
694
|
-
},
|
|
695
|
-
|
|
696
|
-
async loginWithApiKey(apiKey, options = {}) {
|
|
697
|
-
// Validate API key by calling /api/keys/validate
|
|
698
|
-
const url = new URL('/api/keys/validate', API_BASE);
|
|
699
|
-
const isHttps = url.protocol === 'https:';
|
|
700
|
-
const httpModule = isHttps ? https : http;
|
|
701
|
-
const deviceContext = auth.getDeviceContext();
|
|
702
|
-
|
|
703
|
-
return new Promise((resolve, reject) => {
|
|
704
|
-
const requestBody = JSON.stringify({
|
|
705
|
-
apiKey,
|
|
706
|
-
deviceFingerprint: deviceContext.deviceId,
|
|
707
|
-
deviceName: `CLI - ${deviceContext.hostname}`
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
const req = httpModule.request(url, {
|
|
711
|
-
method: 'POST',
|
|
712
|
-
headers: {
|
|
713
|
-
'Content-Type': 'application/json',
|
|
714
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
715
|
-
'Content-Length': Buffer.byteLength(requestBody)
|
|
716
|
-
},
|
|
717
|
-
timeout: 10000
|
|
718
|
-
}, (res) => {
|
|
719
|
-
let body = '';
|
|
720
|
-
res.on('data', chunk => body += chunk);
|
|
721
|
-
res.on('end', async () => {
|
|
722
|
-
try {
|
|
723
|
-
const json = JSON.parse(body);
|
|
724
|
-
if (res.statusCode >= 400 || !json.valid) {
|
|
725
|
-
const error = new Error(redactSensitiveString(json.error || 'Invalid API key'));
|
|
726
|
-
error.status = res.statusCode;
|
|
727
|
-
error.code = json.error;
|
|
728
|
-
reject(error);
|
|
729
|
-
} else {
|
|
730
|
-
const projectId = options?.projectId || json.project?.id || null;
|
|
731
|
-
|
|
732
|
-
// Build user info from response
|
|
733
|
-
const user = {
|
|
734
|
-
tier: json.tier,
|
|
735
|
-
scopes: json.scopes
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
// Save API key and user info
|
|
739
|
-
auth.loginWithApiKey(apiKey, user);
|
|
740
|
-
|
|
741
|
-
// If key has a project, auto-set project context
|
|
742
|
-
if (json.project) {
|
|
743
|
-
session.setCurrentProject(json.project);
|
|
744
|
-
session.addRecentProject(json.project);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (projectId && auth.saveApiKeyToProject) {
|
|
748
|
-
auth.saveApiKeyToProject(apiKey);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Best effort: exchange API key for short-lived project token.
|
|
752
|
-
try {
|
|
753
|
-
const exchanged = await exchangeProjectScopedToken(apiKey, projectId);
|
|
754
|
-
if (auth.saveProjectScopedSession) {
|
|
755
|
-
auth.saveProjectScopedSession(exchanged.token, {
|
|
756
|
-
expiresAt: exchanged.expiresAt,
|
|
757
|
-
source: exchanged.sourcePath,
|
|
758
|
-
migratedFromLegacyApiKey: true
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
} catch {
|
|
762
|
-
// Keep project-bound API key fallback when scoped exchange is unavailable.
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
resolve({
|
|
766
|
-
user,
|
|
767
|
-
project: json.project,
|
|
768
|
-
device: json.device,
|
|
769
|
-
usage: json.usage
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
} catch {
|
|
773
|
-
reject(new Error('Invalid response from API'));
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
779
|
-
req.on('timeout', () => {
|
|
780
|
-
req.destroy();
|
|
781
|
-
reject(new Error('Request timeout'));
|
|
782
|
-
});
|
|
783
|
-
req.write(requestBody);
|
|
784
|
-
req.end();
|
|
785
|
-
});
|
|
786
|
-
},
|
|
787
|
-
|
|
788
|
-
async register(email, password, name) {
|
|
789
|
-
const deviceContext = auth.getDeviceContext();
|
|
790
|
-
const response = await request('POST', '/auth/register', {
|
|
791
|
-
email,
|
|
792
|
-
password,
|
|
793
|
-
name,
|
|
794
|
-
device: deviceContext
|
|
795
|
-
});
|
|
796
|
-
auth.login(response);
|
|
797
|
-
return response;
|
|
798
|
-
},
|
|
799
|
-
|
|
800
|
-
async me() {
|
|
801
|
-
requireAuth();
|
|
802
|
-
return request('GET', '/auth/me');
|
|
803
|
-
},
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Validate an API key and get its associated project
|
|
807
|
-
* @param {string} apiKey - The API key to validate
|
|
808
|
-
* @returns {Promise<{valid: boolean, tier: string, project: object|null}>}
|
|
809
|
-
*/
|
|
810
|
-
async validateApiKey(apiKey) {
|
|
811
|
-
const url = new URL('/api/keys/validate', API_BASE);
|
|
812
|
-
const isHttps = url.protocol === 'https:';
|
|
813
|
-
const httpModule = isHttps ? https : http;
|
|
814
|
-
const deviceContext = auth.getDeviceContext();
|
|
815
|
-
|
|
816
|
-
return new Promise((resolve, reject) => {
|
|
817
|
-
const requestBody = JSON.stringify({
|
|
818
|
-
apiKey,
|
|
819
|
-
deviceFingerprint: deviceContext.deviceId,
|
|
820
|
-
deviceName: `CLI - ${deviceContext.hostname}`
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
const req = httpModule.request(url, {
|
|
824
|
-
method: 'POST',
|
|
825
|
-
headers: {
|
|
826
|
-
'Content-Type': 'application/json',
|
|
827
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
828
|
-
'Content-Length': Buffer.byteLength(requestBody)
|
|
829
|
-
},
|
|
830
|
-
timeout: 10000
|
|
831
|
-
}, (res) => {
|
|
832
|
-
let body = '';
|
|
833
|
-
res.on('data', chunk => body += chunk);
|
|
834
|
-
res.on('end', () => {
|
|
835
|
-
try {
|
|
836
|
-
const json = JSON.parse(body);
|
|
837
|
-
if (res.statusCode >= 400 || !json.valid) {
|
|
838
|
-
const error = new Error(redactSensitiveString(json.error || 'Invalid API key'));
|
|
839
|
-
error.status = res.statusCode;
|
|
840
|
-
reject(error);
|
|
841
|
-
} else {
|
|
842
|
-
resolve(json);
|
|
843
|
-
}
|
|
844
|
-
} catch {
|
|
845
|
-
reject(new Error('Invalid response from API'));
|
|
846
|
-
}
|
|
847
|
-
});
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
851
|
-
req.on('timeout', () => {
|
|
852
|
-
req.destroy();
|
|
853
|
-
reject(new Error('Request timeout'));
|
|
854
|
-
});
|
|
855
|
-
req.write(requestBody);
|
|
856
|
-
req.end();
|
|
857
|
-
});
|
|
858
|
-
},
|
|
859
|
-
|
|
860
|
-
async refreshToken() {
|
|
861
|
-
const refreshToken = auth.getRefreshToken();
|
|
862
|
-
if (!refreshToken) {
|
|
863
|
-
throw new Error('No refresh token available');
|
|
864
|
-
}
|
|
865
|
-
const deviceContext = auth.getDeviceContext();
|
|
866
|
-
const response = await request('POST', '/auth/refresh', {
|
|
867
|
-
refreshToken,
|
|
868
|
-
device: deviceContext
|
|
869
|
-
});
|
|
870
|
-
auth.updateTokens(response);
|
|
871
|
-
return response;
|
|
872
|
-
},
|
|
873
|
-
|
|
874
|
-
async logout() {
|
|
875
|
-
if (auth.isAuthenticated()) {
|
|
876
|
-
try {
|
|
877
|
-
const refreshToken = auth.getRefreshToken();
|
|
878
|
-
await request('POST', '/auth/logout', { refreshToken });
|
|
879
|
-
} catch {
|
|
880
|
-
// Ignore logout API errors
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
auth.logout();
|
|
884
|
-
},
|
|
885
|
-
|
|
886
|
-
// Agents
|
|
887
|
-
async listAgents() {
|
|
888
|
-
requireAuth();
|
|
889
|
-
return request('GET', '/agents');
|
|
890
|
-
},
|
|
891
|
-
|
|
892
|
-
async getAgent(id) {
|
|
893
|
-
requireAuth();
|
|
894
|
-
return request('GET', `/agents/${encodeURIComponent(id)}`);
|
|
895
|
-
},
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Get agent context content (requires auth, tier-gated)
|
|
899
|
-
* @param {string} id - Agent ID
|
|
900
|
-
* @returns {Promise<{id, name, description, context, tier, checksum?: string}>}
|
|
901
|
-
*/
|
|
902
|
-
async getAgentContext(id) {
|
|
903
|
-
requireAuth();
|
|
904
|
-
const response = await request('GET', `/agents/${encodeURIComponent(id)}`);
|
|
905
|
-
return response.agent || response;
|
|
906
|
-
},
|
|
907
|
-
|
|
908
|
-
async invokeAgent(id, projectContext, task) {
|
|
909
|
-
requireAuth();
|
|
910
|
-
return request('POST', `/agents/${encodeURIComponent(id)}/invoke`, {
|
|
911
|
-
projectContext,
|
|
912
|
-
task
|
|
913
|
-
});
|
|
914
|
-
},
|
|
915
|
-
|
|
916
|
-
async getAgentCapabilities(id) {
|
|
917
|
-
requireAuth();
|
|
918
|
-
return request('GET', `/agents/${encodeURIComponent(id)}/capabilities`);
|
|
919
|
-
},
|
|
920
|
-
|
|
921
|
-
// Skills
|
|
922
|
-
/**
|
|
923
|
-
* List available skills with tier information
|
|
924
|
-
* @param {object} options - Filter options
|
|
925
|
-
* @returns {Promise<{skills: Array, categories: Array, userTier: string}>}
|
|
926
|
-
*/
|
|
927
|
-
async listSkills(options = {}) {
|
|
928
|
-
requireAuth();
|
|
929
|
-
const params = new URLSearchParams();
|
|
930
|
-
if (options.category) params.append('category', options.category);
|
|
931
|
-
if (options.search) params.append('search', options.search);
|
|
932
|
-
|
|
933
|
-
const query = params.toString();
|
|
934
|
-
return request('GET', `/skills${query ? '?' + query : ''}`);
|
|
935
|
-
},
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* Get skill content (requires auth, tier-gated)
|
|
939
|
-
* @param {string} skillId - Skill ID (e.g., 'api/route-handler')
|
|
940
|
-
* @returns {Promise<{id, name, content, tier, checksum?: string}>}
|
|
941
|
-
*/
|
|
942
|
-
async getSkillContent(skillId) {
|
|
943
|
-
requireAuth();
|
|
944
|
-
const response = await request('GET', `/skills/${skillId}`);
|
|
945
|
-
return response.skill || response;
|
|
946
|
-
},
|
|
947
|
-
|
|
948
|
-
/**
|
|
949
|
-
* @deprecated Use getSkillContent instead
|
|
950
|
-
*/
|
|
951
|
-
async getSkill(category, name) {
|
|
952
|
-
requireAuth();
|
|
953
|
-
return request('GET', `/skills/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
|
|
954
|
-
},
|
|
955
|
-
|
|
956
|
-
async searchSkills(query, options = {}) {
|
|
957
|
-
requireAuth();
|
|
958
|
-
const params = new URLSearchParams();
|
|
959
|
-
params.append('search', query);
|
|
960
|
-
if (options.category) params.append('category', options.category);
|
|
961
|
-
return request('GET', `/skills?${params.toString()}`);
|
|
962
|
-
},
|
|
963
|
-
|
|
964
|
-
// Templates (thin client - served from API)
|
|
965
|
-
/**
|
|
966
|
-
* List available templates in a category
|
|
967
|
-
* @param {string} category - Template category (business, legal, fundraising, etc.)
|
|
968
|
-
* @returns {Promise<{templates: Array}>}
|
|
969
|
-
*/
|
|
970
|
-
async listTemplates(category) {
|
|
971
|
-
requireAuth();
|
|
972
|
-
return request('GET', `/templates/${encodeURIComponent(category)}`);
|
|
973
|
-
},
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Get template content
|
|
977
|
-
* @param {string} category - Template category
|
|
978
|
-
* @param {string} name - Template name/file
|
|
979
|
-
* @returns {Promise<{name, content, variables}>}
|
|
980
|
-
*/
|
|
981
|
-
async getTemplate(category, name) {
|
|
982
|
-
requireAuth();
|
|
983
|
-
return request('GET', `/templates/${encodeURIComponent(category)}/${encodeURIComponent(name)}`);
|
|
984
|
-
},
|
|
985
|
-
|
|
986
|
-
// Orchestrator
|
|
987
|
-
async listWorkflows() {
|
|
988
|
-
requireAuth();
|
|
989
|
-
return request('GET', '/orchestrator/workflows');
|
|
990
|
-
},
|
|
991
|
-
|
|
992
|
-
async getWorkflow(id) {
|
|
993
|
-
requireAuth();
|
|
994
|
-
return request('GET', `/orchestrator/workflows/${encodeURIComponent(id)}`);
|
|
995
|
-
},
|
|
996
|
-
|
|
997
|
-
async analyzeContext(projectContext, currentTask, recentActions) {
|
|
998
|
-
requireAuth();
|
|
999
|
-
return request('POST', '/orchestrator/analyze', {
|
|
1000
|
-
projectContext,
|
|
1001
|
-
currentTask,
|
|
1002
|
-
recentActions
|
|
1003
|
-
});
|
|
1004
|
-
},
|
|
1005
|
-
|
|
1006
|
-
async startWorkflow(workflowId, projectContext, parameters) {
|
|
1007
|
-
requireAuth();
|
|
1008
|
-
return request('POST', '/orchestrator/start', {
|
|
1009
|
-
workflow: workflowId,
|
|
1010
|
-
projectContext,
|
|
1011
|
-
parameters
|
|
1012
|
-
});
|
|
1013
|
-
},
|
|
1014
|
-
|
|
1015
|
-
async getSuggestions(context, action) {
|
|
1016
|
-
requireAuth();
|
|
1017
|
-
return request('POST', '/orchestrator/suggest', { context, action });
|
|
1018
|
-
},
|
|
1019
|
-
|
|
1020
|
-
// Quality
|
|
1021
|
-
async listQualityGates() {
|
|
1022
|
-
requireAuth();
|
|
1023
|
-
return request('GET', '/quality/gates');
|
|
1024
|
-
},
|
|
1025
|
-
|
|
1026
|
-
async runQualityGate(gateId, projectContext, options) {
|
|
1027
|
-
requireAuth();
|
|
1028
|
-
return request('POST', '/quality/run', {
|
|
1029
|
-
gate: gateId,
|
|
1030
|
-
projectContext,
|
|
1031
|
-
options
|
|
1032
|
-
});
|
|
1033
|
-
},
|
|
1034
|
-
|
|
1035
|
-
async getLintBudgets() {
|
|
1036
|
-
requireAuth();
|
|
1037
|
-
return request('GET', '/quality/lint-budgets');
|
|
1038
|
-
},
|
|
1039
|
-
|
|
1040
|
-
// MCP
|
|
1041
|
-
async listMcpTools() {
|
|
1042
|
-
requireAuth();
|
|
1043
|
-
return request('GET', '/mcp/tools');
|
|
1044
|
-
},
|
|
1045
|
-
|
|
1046
|
-
async callMcpTool(tool, args) {
|
|
1047
|
-
requireAuth();
|
|
1048
|
-
return request('POST', '/mcp/tool', { tool, arguments: args });
|
|
1049
|
-
},
|
|
1050
|
-
|
|
1051
|
-
async listMcpResources() {
|
|
1052
|
-
requireAuth();
|
|
1053
|
-
return request('GET', '/mcp/resources');
|
|
1054
|
-
},
|
|
1055
|
-
|
|
1056
|
-
async getMcpResource(uri) {
|
|
1057
|
-
requireAuth();
|
|
1058
|
-
const normalizedUri = String(uri || '').replace(/^bootspring:\/\//, '');
|
|
1059
|
-
return request('GET', `/mcp/resources/${encodeURIComponent(normalizedUri)}`);
|
|
1060
|
-
},
|
|
1061
|
-
|
|
1062
|
-
async listMcpConnectors() {
|
|
1063
|
-
requireAuth();
|
|
1064
|
-
return request('GET', '/mcp/connectors', null, { noCache: true });
|
|
1065
|
-
},
|
|
1066
|
-
|
|
1067
|
-
async getActiveMcpConnectorMap() {
|
|
1068
|
-
requireAuth();
|
|
1069
|
-
return request('GET', '/mcp/connectors/active', null, { noCache: true });
|
|
1070
|
-
},
|
|
1071
|
-
|
|
1072
|
-
async setMcpConnectorEnabled(connectorId, enabled) {
|
|
1073
|
-
requireAuth();
|
|
1074
|
-
return request('PATCH', `/mcp/connectors/${encodeURIComponent(connectorId)}`, { enabled });
|
|
1075
|
-
},
|
|
1076
|
-
|
|
1077
|
-
// Billing
|
|
1078
|
-
async getSubscription() {
|
|
1079
|
-
requireAuth();
|
|
1080
|
-
return request('GET', '/billing/subscription');
|
|
1081
|
-
},
|
|
1082
|
-
|
|
1083
|
-
async createCheckout(plan) {
|
|
1084
|
-
requireAuth();
|
|
1085
|
-
return request('POST', '/billing/create-checkout', { plan });
|
|
1086
|
-
},
|
|
1087
|
-
|
|
1088
|
-
async getPortalUrl() {
|
|
1089
|
-
requireAuth();
|
|
1090
|
-
return request('POST', '/billing/portal');
|
|
1091
|
-
},
|
|
1092
|
-
|
|
1093
|
-
async getUsage() {
|
|
1094
|
-
requireAuth();
|
|
1095
|
-
return request('GET', '/billing/usage');
|
|
1096
|
-
},
|
|
1097
|
-
|
|
1098
|
-
async getInvoices() {
|
|
1099
|
-
requireAuth();
|
|
1100
|
-
return request('GET', '/billing/invoices');
|
|
1101
|
-
},
|
|
1102
|
-
|
|
1103
|
-
// Entitlements (v1)
|
|
1104
|
-
async resolveEntitlements() {
|
|
1105
|
-
requireAuth();
|
|
1106
|
-
return request('GET', '/entitlements/resolve', null, { noCache: false });
|
|
1107
|
-
},
|
|
1108
|
-
|
|
1109
|
-
// Usage Tracking (v1)
|
|
1110
|
-
async trackUsage(type, metadata = {}) {
|
|
1111
|
-
requireAuth();
|
|
1112
|
-
return request('POST', '/track', { type, metadata });
|
|
1113
|
-
},
|
|
1114
|
-
|
|
1115
|
-
// Telemetry Batch Upload (v1)
|
|
1116
|
-
async uploadTelemetryBatch(events, batchInfo = {}) {
|
|
1117
|
-
requireAuth();
|
|
1118
|
-
const batchId = batchInfo.id || require('crypto').randomUUID();
|
|
1119
|
-
return request('POST', '/events/batch', {
|
|
1120
|
-
source: 'bootspring',
|
|
1121
|
-
batch: {
|
|
1122
|
-
index: batchInfo.index || 0,
|
|
1123
|
-
total: batchInfo.total || 1,
|
|
1124
|
-
id: batchId
|
|
1125
|
-
},
|
|
1126
|
-
events
|
|
1127
|
-
}, {
|
|
1128
|
-
headers: {
|
|
1129
|
-
'X-Bootspring-Batch-Id': batchId
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1132
|
-
},
|
|
1133
|
-
|
|
1134
|
-
// Projects (from /api/auth/device/projects)
|
|
1135
|
-
async listProjects() {
|
|
1136
|
-
requireAuth();
|
|
1137
|
-
// Projects endpoint is under auth, not v1
|
|
1138
|
-
const url = new URL('/api/auth/device/projects', API_BASE);
|
|
1139
|
-
const isHttps = url.protocol === 'https:';
|
|
1140
|
-
const httpModule = isHttps ? https : http;
|
|
1141
|
-
const authHeaders = await resolveAuthHeaders();
|
|
1142
|
-
|
|
1143
|
-
return new Promise((resolve, reject) => {
|
|
1144
|
-
const headers = {
|
|
1145
|
-
'Content-Type': 'application/json',
|
|
1146
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
1147
|
-
...authHeaders
|
|
1148
|
-
};
|
|
1149
|
-
|
|
1150
|
-
const req = httpModule.request(url, {
|
|
1151
|
-
method: 'GET',
|
|
1152
|
-
headers,
|
|
1153
|
-
timeout: 10000
|
|
1154
|
-
}, (res) => {
|
|
1155
|
-
let body = '';
|
|
1156
|
-
res.on('data', chunk => body += chunk);
|
|
1157
|
-
res.on('end', () => {
|
|
1158
|
-
try {
|
|
1159
|
-
const json = JSON.parse(body);
|
|
1160
|
-
if (res.statusCode >= 400) {
|
|
1161
|
-
const error = new Error(
|
|
1162
|
-
redactSensitiveString(String(json.message || json.error || 'Failed to list projects'))
|
|
1163
|
-
);
|
|
1164
|
-
error.status = res.statusCode;
|
|
1165
|
-
reject(error);
|
|
1166
|
-
} else {
|
|
1167
|
-
resolve(json);
|
|
1168
|
-
}
|
|
1169
|
-
} catch {
|
|
1170
|
-
reject(new Error('Invalid response from API'));
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
});
|
|
1174
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
1175
|
-
req.on('timeout', () => {
|
|
1176
|
-
req.destroy();
|
|
1177
|
-
reject(new Error('Request timeout'));
|
|
1178
|
-
});
|
|
1179
|
-
req.end();
|
|
1180
|
-
});
|
|
1181
|
-
},
|
|
1182
|
-
|
|
1183
|
-
// Project Members Management
|
|
1184
|
-
async getProjectMembers(projectId) {
|
|
1185
|
-
requireAuth();
|
|
1186
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/members`);
|
|
1187
|
-
},
|
|
1188
|
-
|
|
1189
|
-
async addProjectMember(projectId, email, role = 'member') {
|
|
1190
|
-
requireAuth();
|
|
1191
|
-
return directRequest('POST', `/projects/${encodeURIComponent(projectId)}/members`, {
|
|
1192
|
-
email,
|
|
1193
|
-
role
|
|
1194
|
-
});
|
|
1195
|
-
},
|
|
1196
|
-
|
|
1197
|
-
async updateProjectMember(projectId, userId, role) {
|
|
1198
|
-
requireAuth();
|
|
1199
|
-
return directRequest('PATCH', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
|
|
1200
|
-
role
|
|
1201
|
-
});
|
|
1202
|
-
},
|
|
1203
|
-
|
|
1204
|
-
async removeProjectMember(projectId, userId) {
|
|
1205
|
-
requireAuth();
|
|
1206
|
-
return directRequest('DELETE', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`);
|
|
1207
|
-
},
|
|
1208
|
-
|
|
1209
|
-
async transferProjectOwnership(projectId, newOwnerId) {
|
|
1210
|
-
requireAuth();
|
|
1211
|
-
return directRequest('POST', `/projects/${encodeURIComponent(projectId)}/transfer`, {
|
|
1212
|
-
newOwnerId
|
|
1213
|
-
});
|
|
1214
|
-
},
|
|
1215
|
-
|
|
1216
|
-
async listProjectInvitations(projectId) {
|
|
1217
|
-
requireAuth();
|
|
1218
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/invitations`);
|
|
1219
|
-
},
|
|
1220
|
-
|
|
1221
|
-
async inviteProjectMember(projectId, email, role = 'member') {
|
|
1222
|
-
requireAuth();
|
|
1223
|
-
return directRequest('POST', `/projects/${encodeURIComponent(projectId)}/invitations`, {
|
|
1224
|
-
email,
|
|
1225
|
-
role
|
|
1226
|
-
});
|
|
1227
|
-
},
|
|
1228
|
-
|
|
1229
|
-
async respondToProjectInvitation(invitationId, decision) {
|
|
1230
|
-
requireAuth();
|
|
1231
|
-
return directRequest('POST', `/projects/invitations/${encodeURIComponent(invitationId)}/${decision}`);
|
|
1232
|
-
},
|
|
1233
|
-
|
|
1234
|
-
async getProjectActivity(projectId, options = {}) {
|
|
1235
|
-
requireAuth();
|
|
1236
|
-
const limit = Number(options.limit);
|
|
1237
|
-
const effectiveLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
1238
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/activity?limit=${effectiveLimit}`);
|
|
1239
|
-
},
|
|
1240
|
-
|
|
1241
|
-
async findSimilarProjects(name, repoUrl) {
|
|
1242
|
-
requireAuth();
|
|
1243
|
-
const params = new URLSearchParams();
|
|
1244
|
-
if (name) params.set('name', name);
|
|
1245
|
-
if (repoUrl) params.set('repo', repoUrl);
|
|
1246
|
-
return directRequest('GET', `/projects/similar?${params.toString()}`);
|
|
1247
|
-
},
|
|
1248
|
-
|
|
1249
|
-
// Preseed Documents
|
|
1250
|
-
/**
|
|
1251
|
-
* List preseed documents for a project
|
|
1252
|
-
* @param {string} projectId - Project ID
|
|
1253
|
-
* @returns {Promise<{documents: Array, count: number, storage: object}>}
|
|
1254
|
-
*/
|
|
1255
|
-
async listPreseedDocuments(projectId) {
|
|
1256
|
-
requireAuth();
|
|
1257
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/preseed/documents`);
|
|
1258
|
-
},
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Get a single preseed document content
|
|
1262
|
-
* @param {string} projectId - Project ID
|
|
1263
|
-
* @param {string} documentName - Document name (e.g., 'VISION', 'PRD')
|
|
1264
|
-
* @returns {Promise<{name: string, content: string, format: string}>}
|
|
1265
|
-
*/
|
|
1266
|
-
async getPreseedDocument(projectId, documentName) {
|
|
1267
|
-
requireAuth();
|
|
1268
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/preseed/documents?name=${encodeURIComponent(documentName)}`);
|
|
1269
|
-
},
|
|
1270
|
-
|
|
1271
|
-
/**
|
|
1272
|
-
* Download preseed documents as ZIP
|
|
1273
|
-
* @param {string} projectId - Project ID
|
|
1274
|
-
* @returns {Promise<Buffer>} ZIP file buffer
|
|
1275
|
-
*/
|
|
1276
|
-
async downloadPreseedZip(projectId) {
|
|
1277
|
-
requireAuth();
|
|
1278
|
-
const authHeaders = await resolveAuthHeaders();
|
|
1279
|
-
const url = new URL(`/api/projects/${encodeURIComponent(projectId)}/preseed/documents?format=zip`, API_BASE);
|
|
1280
|
-
const isHttps = url.protocol === 'https:';
|
|
1281
|
-
const httpModule = isHttps ? https : http;
|
|
1282
|
-
|
|
1283
|
-
return new Promise((resolve, reject) => {
|
|
1284
|
-
const headers = {
|
|
1285
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
1286
|
-
...authHeaders
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
const req = httpModule.request(url, {
|
|
1290
|
-
method: 'GET',
|
|
1291
|
-
headers,
|
|
1292
|
-
timeout: 60000
|
|
1293
|
-
}, (res) => {
|
|
1294
|
-
if (res.statusCode >= 400) {
|
|
1295
|
-
let body = '';
|
|
1296
|
-
res.on('data', chunk => body += chunk);
|
|
1297
|
-
res.on('end', () => {
|
|
1298
|
-
try {
|
|
1299
|
-
const json = JSON.parse(body);
|
|
1300
|
-
const error = new Error(
|
|
1301
|
-
redactSensitiveString(String(json.message || json.error || 'Failed to download preseed documents'))
|
|
1302
|
-
);
|
|
1303
|
-
error.status = res.statusCode;
|
|
1304
|
-
reject(error);
|
|
1305
|
-
} catch {
|
|
1306
|
-
reject(new Error(redactSensitiveString(`HTTP ${res.statusCode}: ${body}`)));
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
const chunks = [];
|
|
1313
|
-
res.on('data', chunk => chunks.push(chunk));
|
|
1314
|
-
res.on('end', () => {
|
|
1315
|
-
resolve(Buffer.concat(chunks));
|
|
1316
|
-
});
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
1320
|
-
req.on('timeout', () => {
|
|
1321
|
-
req.destroy();
|
|
1322
|
-
reject(new Error('Request timeout'));
|
|
1323
|
-
});
|
|
1324
|
-
req.end();
|
|
1325
|
-
});
|
|
1326
|
-
},
|
|
1327
|
-
|
|
1328
|
-
/**
|
|
1329
|
-
* Get preseed wizard data for a project
|
|
1330
|
-
* @param {string} projectId - Project ID
|
|
1331
|
-
* @returns {Promise<{wizardData: object, progress: object}>}
|
|
1332
|
-
*/
|
|
1333
|
-
async getPreseedWizard(projectId) {
|
|
1334
|
-
requireAuth();
|
|
1335
|
-
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/preseed/wizard`);
|
|
1336
|
-
},
|
|
1337
|
-
|
|
1338
|
-
/**
|
|
1339
|
-
* Enrich preseed content generated from codebase analysis.
|
|
1340
|
-
* Uses a versioned contract and endpoint fallback for bootspring-site rollouts.
|
|
1341
|
-
* @param {object} payload - Enrichment request payload
|
|
1342
|
-
* @param {object} options - Request options
|
|
1343
|
-
* @returns {Promise<object>} Enrichment response
|
|
1344
|
-
*/
|
|
1345
|
-
async enrichCodebasePreseed(payload = {}, options = {}) {
|
|
1346
|
-
requireAuth();
|
|
1347
|
-
|
|
1348
|
-
const contractVersion = typeof payload.contractVersion === 'string' && payload.contractVersion.trim()
|
|
1349
|
-
? payload.contractVersion.trim()
|
|
1350
|
-
: 'preseed-enrichment.v1';
|
|
1351
|
-
|
|
1352
|
-
const body = {
|
|
1353
|
-
...payload,
|
|
1354
|
-
contractVersion
|
|
1355
|
-
};
|
|
1356
|
-
|
|
1357
|
-
const requestedPaths = Array.isArray(options.paths)
|
|
1358
|
-
? options.paths.filter((candidate) => typeof candidate === 'string' && candidate.trim())
|
|
1359
|
-
: [];
|
|
1360
|
-
const candidatePaths = [...new Set([...requestedPaths, ...PRESEED_CODEBASE_ENRICHMENT_PATHS])];
|
|
1361
|
-
|
|
1362
|
-
const requestOptions = {
|
|
1363
|
-
timeout: Number(options.timeout) > 0 ? Number(options.timeout) : 60000,
|
|
1364
|
-
noCache: true,
|
|
1365
|
-
headers: {
|
|
1366
|
-
...(options.headers || {}),
|
|
1367
|
-
'X-Bootspring-Contract': contractVersion
|
|
1368
|
-
}
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
let lastError = null;
|
|
1372
|
-
|
|
1373
|
-
for (const endpoint of candidatePaths) {
|
|
1374
|
-
try {
|
|
1375
|
-
return await request('POST', endpoint, body, requestOptions);
|
|
1376
|
-
} catch (error) {
|
|
1377
|
-
lastError = error;
|
|
1378
|
-
const status = Number(error?.status);
|
|
1379
|
-
if (status === 404 || status === 405 || status === 501) {
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
throw error;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
const unavailable = new Error('Remote enrichment endpoint unavailable');
|
|
1387
|
-
unavailable.code = 'ENRICHMENT_ENDPOINT_UNAVAILABLE';
|
|
1388
|
-
unavailable.details = { attemptedPaths: candidatePaths };
|
|
1389
|
-
if (lastError && typeof lastError.status === 'number') {
|
|
1390
|
-
unavailable.status = lastError.status;
|
|
1391
|
-
}
|
|
1392
|
-
throw unavailable;
|
|
1393
|
-
},
|
|
1394
|
-
|
|
1395
|
-
// =====================================================
|
|
1396
|
-
// Organization Methods
|
|
1397
|
-
// =====================================================
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* List organizations for current user
|
|
1401
|
-
* @returns {Promise<Array>} List of organizations
|
|
1402
|
-
*/
|
|
1403
|
-
async listOrganizations() {
|
|
1404
|
-
requireAuth();
|
|
1405
|
-
return request('GET', '/organizations');
|
|
1406
|
-
},
|
|
1407
|
-
|
|
1408
|
-
/**
|
|
1409
|
-
* Get organization details
|
|
1410
|
-
* @param {string} orgId - Organization ID
|
|
1411
|
-
* @returns {Promise<object>} Organization details with members
|
|
1412
|
-
*/
|
|
1413
|
-
async getOrganization(orgId) {
|
|
1414
|
-
requireAuth();
|
|
1415
|
-
return request('GET', `/organizations/${encodeURIComponent(orgId)}`);
|
|
1416
|
-
},
|
|
1417
|
-
|
|
1418
|
-
/**
|
|
1419
|
-
* Get organization policy
|
|
1420
|
-
* @param {string} orgId - Organization ID
|
|
1421
|
-
* @returns {Promise<object>} Organization policy settings
|
|
1422
|
-
*/
|
|
1423
|
-
async getOrgPolicy(orgId) {
|
|
1424
|
-
requireAuth();
|
|
1425
|
-
return request('GET', `/organizations/${encodeURIComponent(orgId)}/policy`);
|
|
1426
|
-
},
|
|
1427
|
-
|
|
1428
|
-
/**
|
|
1429
|
-
* Update organization policy
|
|
1430
|
-
* @param {string} orgId - Organization ID
|
|
1431
|
-
* @param {object} policy - Policy settings to update
|
|
1432
|
-
* @returns {Promise<object>} Updated policy
|
|
1433
|
-
*/
|
|
1434
|
-
async updateOrgPolicy(orgId, policy) {
|
|
1435
|
-
requireAuth();
|
|
1436
|
-
return request('PATCH', `/organizations/${encodeURIComponent(orgId)}/policy`, policy);
|
|
1437
|
-
},
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* List organization members
|
|
1441
|
-
* @param {string} orgId - Organization ID
|
|
1442
|
-
* @returns {Promise<Array>} List of members
|
|
1443
|
-
*/
|
|
1444
|
-
async listOrgMembers(orgId) {
|
|
1445
|
-
requireAuth();
|
|
1446
|
-
return request('GET', `/organizations/${encodeURIComponent(orgId)}/members`);
|
|
1447
|
-
},
|
|
1448
|
-
|
|
1449
|
-
/**
|
|
1450
|
-
* Get member policy overrides
|
|
1451
|
-
* @param {string} orgId - Organization ID
|
|
1452
|
-
* @param {string} userId - User ID
|
|
1453
|
-
* @returns {Promise<object>} Member policy overrides
|
|
1454
|
-
*/
|
|
1455
|
-
async getMemberPolicy(orgId, userId) {
|
|
1456
|
-
requireAuth();
|
|
1457
|
-
return request('GET', `/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}/policy`);
|
|
1458
|
-
},
|
|
1459
|
-
|
|
1460
|
-
/**
|
|
1461
|
-
* Update member policy overrides
|
|
1462
|
-
* @param {string} orgId - Organization ID
|
|
1463
|
-
* @param {string} userId - User ID
|
|
1464
|
-
* @param {object} policy - Policy overrides
|
|
1465
|
-
* @returns {Promise<object>} Updated member policy
|
|
1466
|
-
*/
|
|
1467
|
-
async updateMemberPolicy(orgId, userId, policy) {
|
|
1468
|
-
requireAuth();
|
|
1469
|
-
return request('PATCH', `/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}/policy`, policy);
|
|
1470
|
-
}
|
|
1471
|
-
};
|
|
1472
|
-
|
|
1473
|
-
module.exports = {
|
|
1474
|
-
API_BASE,
|
|
1475
|
-
API_VERSION,
|
|
1476
|
-
request,
|
|
1477
|
-
directRequest,
|
|
1478
|
-
clearCache,
|
|
1479
|
-
healthCheck,
|
|
1480
|
-
requireAuth,
|
|
1481
|
-
...api
|
|
1482
|
-
};
|