@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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/bin/bootspring.js +35 -96
  3. package/claude-commands/agent.md +34 -0
  4. package/claude-commands/bs.md +31 -0
  5. package/claude-commands/build.md +25 -0
  6. package/claude-commands/skill.md +31 -0
  7. package/claude-commands/todo.md +25 -0
  8. package/dist/cli/index.cjs +17808 -0
  9. package/dist/core/index.d.ts +5814 -0
  10. package/dist/core.js +5780 -0
  11. package/dist/mcp/index.d.ts +1 -0
  12. package/dist/mcp-server.js +2299 -0
  13. package/generators/api-docs.js +2 -2
  14. package/generators/decisions.js +3 -3
  15. package/generators/health.js +16 -16
  16. package/generators/sprint.js +2 -2
  17. package/package.json +27 -59
  18. package/core/api-client.d.ts +0 -69
  19. package/core/api-client.js +0 -1482
  20. package/core/auth.d.ts +0 -98
  21. package/core/auth.js +0 -737
  22. package/core/build-orchestrator.js +0 -508
  23. package/core/build-state.js +0 -612
  24. package/core/config.d.ts +0 -106
  25. package/core/config.js +0 -1328
  26. package/core/context-loader.js +0 -580
  27. package/core/context.d.ts +0 -61
  28. package/core/context.js +0 -327
  29. package/core/entitlements.d.ts +0 -70
  30. package/core/entitlements.js +0 -322
  31. package/core/index.d.ts +0 -53
  32. package/core/index.js +0 -62
  33. package/core/mcp-config.js +0 -115
  34. package/core/policies.d.ts +0 -43
  35. package/core/policies.js +0 -113
  36. package/core/policy-matrix.js +0 -303
  37. package/core/project-activity.js +0 -175
  38. package/core/redaction.d.ts +0 -5
  39. package/core/redaction.js +0 -63
  40. package/core/self-update.js +0 -259
  41. package/core/session.js +0 -353
  42. package/core/task-extractor.js +0 -1098
  43. package/core/telemetry.d.ts +0 -55
  44. package/core/telemetry.js +0 -617
  45. package/core/tier-enforcement.js +0 -928
  46. package/core/utils.d.ts +0 -90
  47. package/core/utils.js +0 -455
  48. package/core/validation.js +0 -572
  49. package/mcp/server.d.ts +0 -57
  50. package/mcp/server.js +0 -264
@@ -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
- };