@hyperdrive.bot/cli 1.0.2

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 (127) hide show
  1. package/README.md +1598 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/account/add.d.ts +16 -0
  7. package/dist/commands/account/add.js +185 -0
  8. package/dist/commands/account/list.d.ts +6 -0
  9. package/dist/commands/account/list.js +37 -0
  10. package/dist/commands/account/remove.d.ts +11 -0
  11. package/dist/commands/account/remove.js +57 -0
  12. package/dist/commands/auth/login.d.ts +16 -0
  13. package/dist/commands/auth/login.js +178 -0
  14. package/dist/commands/auth/logout.d.ts +6 -0
  15. package/dist/commands/auth/logout.js +39 -0
  16. package/dist/commands/auth/refresh.d.ts +6 -0
  17. package/dist/commands/auth/refresh.js +66 -0
  18. package/dist/commands/auth/status.d.ts +6 -0
  19. package/dist/commands/auth/status.js +63 -0
  20. package/dist/commands/ci/account/create.d.ts +16 -0
  21. package/dist/commands/ci/account/create.js +158 -0
  22. package/dist/commands/ci/account/delete.d.ts +14 -0
  23. package/dist/commands/ci/account/delete.js +88 -0
  24. package/dist/commands/ci/account/list.d.ts +10 -0
  25. package/dist/commands/ci/account/list.js +65 -0
  26. package/dist/commands/config/get.d.ts +9 -0
  27. package/dist/commands/config/get.js +37 -0
  28. package/dist/commands/config/set.d.ts +10 -0
  29. package/dist/commands/config/set.js +48 -0
  30. package/dist/commands/config/show.d.ts +6 -0
  31. package/dist/commands/config/show.js +10 -0
  32. package/dist/commands/deployment/create.d.ts +30 -0
  33. package/dist/commands/deployment/create.js +188 -0
  34. package/dist/commands/deployment/get.d.ts +13 -0
  35. package/dist/commands/deployment/get.js +101 -0
  36. package/dist/commands/deployment/launch.d.ts +15 -0
  37. package/dist/commands/deployment/launch.js +105 -0
  38. package/dist/commands/deployment/list.d.ts +11 -0
  39. package/dist/commands/deployment/list.js +91 -0
  40. package/dist/commands/domain/current.d.ts +6 -0
  41. package/dist/commands/domain/current.js +18 -0
  42. package/dist/commands/domain/list.d.ts +6 -0
  43. package/dist/commands/domain/list.js +42 -0
  44. package/dist/commands/domain/switch.d.ts +9 -0
  45. package/dist/commands/domain/switch.js +40 -0
  46. package/dist/commands/example.d.ts +13 -0
  47. package/dist/commands/example.js +24 -0
  48. package/dist/commands/git/connect.d.ts +10 -0
  49. package/dist/commands/git/connect.js +56 -0
  50. package/dist/commands/git/disconnect.d.ts +11 -0
  51. package/dist/commands/git/disconnect.js +93 -0
  52. package/dist/commands/git/list.d.ts +10 -0
  53. package/dist/commands/git/list.js +53 -0
  54. package/dist/commands/git/sync.d.ts +18 -0
  55. package/dist/commands/git/sync.js +235 -0
  56. package/dist/commands/init.d.ts +188 -0
  57. package/dist/commands/init.js +817 -0
  58. package/dist/commands/jira/connect.d.ts +9 -0
  59. package/dist/commands/jira/connect.js +141 -0
  60. package/dist/commands/jira/status.d.ts +9 -0
  61. package/dist/commands/jira/status.js +118 -0
  62. package/dist/commands/module/analyze.d.ts +29 -0
  63. package/dist/commands/module/analyze.js +201 -0
  64. package/dist/commands/module/create.d.ts +42 -0
  65. package/dist/commands/module/create.js +498 -0
  66. package/dist/commands/module/destroy.d.ts +11 -0
  67. package/dist/commands/module/destroy.js +77 -0
  68. package/dist/commands/module/get.d.ts +10 -0
  69. package/dist/commands/module/get.js +43 -0
  70. package/dist/commands/module/link.d.ts +15 -0
  71. package/dist/commands/module/link.js +175 -0
  72. package/dist/commands/module/list.d.ts +9 -0
  73. package/dist/commands/module/list.js +51 -0
  74. package/dist/commands/module/reanalyze.d.ts +30 -0
  75. package/dist/commands/module/reanalyze.js +206 -0
  76. package/dist/commands/module/update.d.ts +27 -0
  77. package/dist/commands/module/update.js +102 -0
  78. package/dist/commands/parameter/add.d.ts +15 -0
  79. package/dist/commands/parameter/add.js +99 -0
  80. package/dist/commands/parameter/backfill.d.ts +12 -0
  81. package/dist/commands/parameter/backfill.js +113 -0
  82. package/dist/commands/parameter/clear.d.ts +14 -0
  83. package/dist/commands/parameter/clear.js +95 -0
  84. package/dist/commands/parameter/list.d.ts +14 -0
  85. package/dist/commands/parameter/list.js +92 -0
  86. package/dist/commands/parameter/pull.d.ts +14 -0
  87. package/dist/commands/parameter/pull.js +124 -0
  88. package/dist/commands/parameter/remove.d.ts +15 -0
  89. package/dist/commands/parameter/remove.js +90 -0
  90. package/dist/commands/parameter/sync.d.ts +14 -0
  91. package/dist/commands/parameter/sync.js +153 -0
  92. package/dist/commands/parameter/update.d.ts +15 -0
  93. package/dist/commands/parameter/update.js +100 -0
  94. package/dist/commands/stage/create.d.ts +28 -0
  95. package/dist/commands/stage/create.js +312 -0
  96. package/dist/commands/stage/list.d.ts +9 -0
  97. package/dist/commands/stage/list.js +63 -0
  98. package/dist/commands/test-api.d.ts +9 -0
  99. package/dist/commands/test-api.js +40 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/services/auth-service.d.ts +84 -0
  103. package/dist/services/auth-service.js +240 -0
  104. package/dist/services/git.d.ts +46 -0
  105. package/dist/services/git.js +409 -0
  106. package/dist/services/hyperdrive-sigv4.d.ts +449 -0
  107. package/dist/services/hyperdrive-sigv4.js +375 -0
  108. package/dist/services/hyperdrive.d.ts +87 -0
  109. package/dist/services/hyperdrive.js +108 -0
  110. package/dist/services/log-tailer.d.ts +95 -0
  111. package/dist/services/log-tailer.js +242 -0
  112. package/dist/services/tenant-service.d.ts +106 -0
  113. package/dist/services/tenant-service.js +332 -0
  114. package/dist/utils/account-flow.d.ts +74 -0
  115. package/dist/utils/account-flow.js +228 -0
  116. package/dist/utils/auth-flow.d.ts +146 -0
  117. package/dist/utils/auth-flow.js +477 -0
  118. package/dist/utils/git-flow.d.ts +72 -0
  119. package/dist/utils/git-flow.js +232 -0
  120. package/dist/utils/jira-flow.d.ts +71 -0
  121. package/dist/utils/jira-flow.js +120 -0
  122. package/dist/utils/summary-display.d.ts +59 -0
  123. package/dist/utils/summary-display.js +140 -0
  124. package/dist/utils/validation.d.ts +15 -0
  125. package/dist/utils/validation.js +32 -0
  126. package/oclif.manifest.json +2819 -0
  127. package/package.json +112 -0
@@ -0,0 +1,477 @@
1
+ import { CognitoIdentityClient, GetCredentialsForIdentityCommand, GetIdCommand } from '@aws-sdk/client-cognito-identity';
2
+ import { CognitoIdentityProviderClient, InitiateAuthCommand, } from '@aws-sdk/client-cognito-identity-provider';
3
+ import axios from 'axios';
4
+ import crypto from 'crypto';
5
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
6
+ import { createServer } from 'http';
7
+ import open from 'open';
8
+ import { homedir } from 'os';
9
+ import { join } from 'path';
10
+ import { parse } from 'url';
11
+ import { TenantService } from '../services/tenant-service.js';
12
+ /**
13
+ * Generate PKCE code verifier (random base64url string)
14
+ */
15
+ export function generateCodeVerifier() {
16
+ return crypto.randomBytes(32).toString('base64url');
17
+ }
18
+ /**
19
+ * Generate PKCE code challenge (SHA256 hash of verifier)
20
+ */
21
+ export function generateCodeChallenge(verifier) {
22
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
23
+ }
24
+ /**
25
+ * Build Cognito authorization URL with PKCE parameters
26
+ */
27
+ export function buildAuthUrl(tenantConfig, codeChallenge, port) {
28
+ // CLI only needs these scopes to authenticate and get AWS credentials via Identity Pool
29
+ // - openid: Required for OIDC authentication
30
+ // - email: User's email address (used by Identity Pool)
31
+ // - profile: User's profile information
32
+ const requiredScopes = 'openid email profile';
33
+ const params = new URLSearchParams({
34
+ client_id: tenantConfig.cognitoClientId,
35
+ code_challenge: codeChallenge,
36
+ code_challenge_method: 'S256',
37
+ redirect_uri: `http://localhost:${port}/callback`,
38
+ response_type: 'code',
39
+ scope: requiredScopes,
40
+ });
41
+ return `https://${tenantConfig.cognitoDomain}/oauth2/authorize?${params}`;
42
+ }
43
+ /**
44
+ * Start local HTTP server to receive OAuth callback
45
+ * Returns a promise that resolves with the authorization code
46
+ */
47
+ export function startCallbackServer(port, timeout) {
48
+ return new Promise((resolve, reject) => {
49
+ let timeoutId = null;
50
+ const server = createServer((req, res) => {
51
+ const { pathname, query } = parse(req.url || '', true);
52
+ if (pathname === '/callback') {
53
+ if (timeoutId) {
54
+ clearTimeout(timeoutId);
55
+ }
56
+ if (query.code) {
57
+ // Success response
58
+ res.writeHead(200, { 'Content-Type': 'text/html' });
59
+ res.end(`
60
+ <!DOCTYPE html>
61
+ <html>
62
+ <head>
63
+ <title>Hyperdrive - Authentication Successful</title>
64
+ <style>
65
+ body {
66
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ height: 100vh;
71
+ margin: 0;
72
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
73
+ }
74
+ .container {
75
+ text-align: center;
76
+ background: white;
77
+ padding: 3rem;
78
+ border-radius: 1rem;
79
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
80
+ max-width: 400px;
81
+ }
82
+ .checkmark {
83
+ width: 80px;
84
+ height: 80px;
85
+ border-radius: 50%;
86
+ display: block;
87
+ stroke-width: 4;
88
+ stroke: #10B981;
89
+ stroke-miterlimit: 10;
90
+ margin: 0 auto 1.5rem;
91
+ animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
92
+ }
93
+ .checkmark-circle {
94
+ stroke-dasharray: 166;
95
+ stroke-dashoffset: 166;
96
+ stroke-width: 4;
97
+ stroke-miterlimit: 10;
98
+ stroke: #10B981;
99
+ fill: none;
100
+ animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
101
+ }
102
+ .checkmark-check {
103
+ transform-origin: 50% 50%;
104
+ stroke-dasharray: 48;
105
+ stroke-dashoffset: 48;
106
+ animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
107
+ }
108
+ @keyframes stroke {
109
+ 100% { stroke-dashoffset: 0; }
110
+ }
111
+ @keyframes scale {
112
+ 0%, 100% { transform: none; }
113
+ 50% { transform: scale3d(1.1, 1.1, 1); }
114
+ }
115
+ h1 { color: #1F2937; margin: 0 0 0.5rem; }
116
+ p { color: #6B7280; margin: 0 0 1.5rem; }
117
+ .close-btn {
118
+ background: #667eea;
119
+ color: white;
120
+ border: none;
121
+ padding: 0.75rem 2rem;
122
+ border-radius: 0.5rem;
123
+ font-size: 1rem;
124
+ cursor: pointer;
125
+ transition: background 0.2s;
126
+ }
127
+ .close-btn:hover { background: #5a67d8; }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="container">
132
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
133
+ <circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
134
+ <path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
135
+ </svg>
136
+ <h1>Authentication Successful!</h1>
137
+ <p>You can close this window and return to the CLI.</p>
138
+ <button class="close-btn" onclick="window.close()">Close Window</button>
139
+ </div>
140
+ <script>setTimeout(() => window.close(), 3000);</script>
141
+ </body>
142
+ </html>
143
+ `);
144
+ server.close();
145
+ resolve(query.code);
146
+ }
147
+ else if (query.error) {
148
+ // Error response
149
+ res.writeHead(400, { 'Content-Type': 'text/html' });
150
+ res.end(`
151
+ <h1>Authentication Failed</h1>
152
+ <p>Error: ${query.error}</p>
153
+ <p>Description: ${query.error_description || 'Unknown error'}</p>
154
+ `);
155
+ server.close();
156
+ reject(new Error(`Authentication failed: ${query.error}`));
157
+ }
158
+ }
159
+ else {
160
+ res.writeHead(404);
161
+ res.end('Not Found');
162
+ }
163
+ });
164
+ server.listen(port, 'localhost');
165
+ server.on('error', (err) => {
166
+ if (timeoutId) {
167
+ clearTimeout(timeoutId);
168
+ }
169
+ reject(new Error(`Failed to start callback server: ${err.message}`));
170
+ });
171
+ // Set timeout for authentication
172
+ timeoutId = setTimeout(() => {
173
+ server.close();
174
+ reject(new Error('Authentication timed out. Please try again.'));
175
+ }, timeout);
176
+ });
177
+ }
178
+ /**
179
+ * Exchange authorization code for Cognito tokens
180
+ */
181
+ export async function exchangeCodeForTokens(tenantConfig, code, codeVerifier, port) {
182
+ try {
183
+ const response = await axios.post(`https://${tenantConfig.cognitoDomain}/oauth2/token`, new URLSearchParams({
184
+ client_id: tenantConfig.cognitoClientId,
185
+ code,
186
+ code_verifier: codeVerifier,
187
+ grant_type: 'authorization_code',
188
+ redirect_uri: `http://localhost:${port}/callback`,
189
+ }), {
190
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
191
+ });
192
+ return response.data;
193
+ }
194
+ catch (error) {
195
+ if (axios.isAxiosError(error)) {
196
+ throw new Error(`Token exchange failed: ${error.response?.data?.error_description || error.message}`);
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+ /**
202
+ * Get AWS credentials from Cognito Identity Pool
203
+ */
204
+ export async function getAWSCredentials(tenantConfig, idToken) {
205
+ const client = new CognitoIdentityClient({ region: tenantConfig.region });
206
+ try {
207
+ // Step 1: Get Identity ID
208
+ const getIdResponse = await client.send(new GetIdCommand({
209
+ IdentityPoolId: tenantConfig.cognitoIdentityPoolId,
210
+ Logins: {
211
+ [`cognito-idp.${tenantConfig.region}.amazonaws.com/${tenantConfig.cognitoUserPoolId}`]: idToken,
212
+ },
213
+ }));
214
+ if (!getIdResponse.IdentityId) {
215
+ throw new Error('Failed to get Identity ID from Cognito');
216
+ }
217
+ // Step 2: Get temporary AWS credentials
218
+ const getCredentialsResponse = await client.send(new GetCredentialsForIdentityCommand({
219
+ IdentityId: getIdResponse.IdentityId,
220
+ Logins: {
221
+ [`cognito-idp.${tenantConfig.region}.amazonaws.com/${tenantConfig.cognitoUserPoolId}`]: idToken,
222
+ },
223
+ }));
224
+ if (!getCredentialsResponse.Credentials) {
225
+ throw new Error('Failed to get AWS credentials from Cognito');
226
+ }
227
+ const { AccessKeyId, Expiration, SecretKey, SessionToken } = getCredentialsResponse.Credentials;
228
+ if (!AccessKeyId || !SecretKey || !SessionToken || !Expiration) {
229
+ throw new Error('Incomplete AWS credentials received from Cognito');
230
+ }
231
+ return {
232
+ accessKeyId: AccessKeyId,
233
+ expiration: Expiration,
234
+ secretAccessKey: SecretKey,
235
+ sessionToken: SessionToken,
236
+ };
237
+ }
238
+ catch (error) {
239
+ if (error instanceof Error) {
240
+ throw new Error(`Failed to obtain AWS credentials: ${error.message}`);
241
+ }
242
+ throw error;
243
+ }
244
+ }
245
+ /**
246
+ * Save credentials to domain-specific path (always required)
247
+ */
248
+ export function saveCredentials(credentials, domain) {
249
+ // Prevent saving test/mock credentials to production path
250
+ const testPatterns = ['test-client-id', 'us-east-1_TEST', 'test-identity', 'test.auth.amazoncognito'];
251
+ const credString = JSON.stringify(credentials);
252
+ for (const pattern of testPatterns) {
253
+ if (credString.includes(pattern)) {
254
+ throw new Error(`Refusing to save credentials containing test value: "${pattern}". This looks like test data.`);
255
+ }
256
+ }
257
+ if (!domain) {
258
+ throw new Error('Domain is required to save credentials');
259
+ }
260
+ const credDir = join(homedir(), '.hyperdrive');
261
+ const credPath = join(credDir, `credentials.${domain}`);
262
+ // Create directory if it doesn't exist
263
+ if (!existsSync(credDir)) {
264
+ mkdirSync(credDir, { recursive: true });
265
+ }
266
+ // Write credentials with secure file permissions
267
+ writeFileSync(credPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
268
+ }
269
+ /**
270
+ * Get the path to the credentials file for a domain
271
+ */
272
+ export function getCredentialsPath(domain) {
273
+ if (!domain) {
274
+ throw new Error('Domain is required to get credentials path');
275
+ }
276
+ return join(homedir(), '.hyperdrive', `credentials.${domain}`);
277
+ }
278
+ /**
279
+ * Execute the complete OAuth PKCE authentication flow
280
+ *
281
+ * This function orchestrates the entire authentication flow:
282
+ * 1. Bootstrap tenant to get Cognito config
283
+ * 2. Generate PKCE codes
284
+ * 3. Start callback server
285
+ * 4. Open browser for authentication
286
+ * 5. Wait for callback with timeout
287
+ * 6. Exchange code for tokens
288
+ * 7. Get AWS credentials from Identity Pool
289
+ * 8. Save credentials to file
290
+ *
291
+ * @param options - Configuration options for the auth flow
292
+ * @returns AuthResult indicating success or failure
293
+ */
294
+ export async function executeAuthFlow(options) {
295
+ const { callbackPort = 8765, logger, tenantDomain, timeout = 300000 } = options;
296
+ const tenantService = new TenantService();
297
+ try {
298
+ // Step 1: Bootstrap tenant to get Cognito config
299
+ const tenantConfig = await tenantService.fetchTenantConfig(tenantDomain);
300
+ // Step 2: Generate PKCE parameters
301
+ const codeVerifier = generateCodeVerifier();
302
+ const codeChallenge = generateCodeChallenge(codeVerifier);
303
+ // Step 3: Start local callback server and get promise for auth code
304
+ const authCodePromise = startCallbackServer(callbackPort, timeout);
305
+ // Step 4: Open browser for authentication
306
+ const authUrl = buildAuthUrl(tenantConfig, codeChallenge, callbackPort);
307
+ // Display URL to user in case browser opens in wrong profile
308
+ if (logger) {
309
+ logger(`Opening browser for authentication...`);
310
+ logger(`If browser doesn't open or opens in wrong profile, copy this URL:`);
311
+ logger(` ${authUrl}`);
312
+ logger('');
313
+ }
314
+ await open(authUrl);
315
+ // Step 5: Wait for callback with auth code
316
+ const code = await authCodePromise;
317
+ // Step 6: Exchange code for tokens
318
+ const tokens = await exchangeCodeForTokens(tenantConfig, code, codeVerifier, callbackPort);
319
+ // Step 7: Get AWS credentials from Cognito Identity Pool
320
+ const awsCredentials = await getAWSCredentials(tenantConfig, tokens.id_token);
321
+ // Step 8: Save credentials
322
+ saveCredentials({
323
+ ...tokens,
324
+ apiUrl: tenantConfig.apiUrl,
325
+ awsCredentials,
326
+ cognitoConfig: {
327
+ clientId: tenantConfig.cognitoClientId,
328
+ domain: tenantConfig.cognitoDomain,
329
+ identityPoolId: tenantConfig.cognitoIdentityPoolId,
330
+ userPoolId: tenantConfig.cognitoUserPoolId,
331
+ },
332
+ obtainedAt: new Date().toISOString(),
333
+ region: tenantConfig.region,
334
+ tenantDomain: tenantConfig.tenantDomain,
335
+ tenantId: tenantConfig.tenantId,
336
+ }, tenantConfig.tenantDomain);
337
+ return { success: true };
338
+ }
339
+ catch (error) {
340
+ const errorMessage = error instanceof Error ? error.message : String(error);
341
+ return { error: errorMessage, success: false };
342
+ }
343
+ }
344
+ /**
345
+ * Execute CI authentication flow using USER_PASSWORD_AUTH
346
+ *
347
+ * This is for non-interactive CI/CD environments where browser-based
348
+ * OAuth is not possible. Uses Cognito's USER_PASSWORD_AUTH flow.
349
+ *
350
+ * @param options - CI authentication options
351
+ * @returns AuthResult indicating success or failure
352
+ */
353
+ export async function executeCIAuthFlow(options) {
354
+ const { logger, password, tenantDomain, username } = options;
355
+ const tenantService = new TenantService();
356
+ try {
357
+ // Step 1: Bootstrap tenant to get Cognito config
358
+ if (logger)
359
+ logger('Fetching tenant configuration...');
360
+ const tenantConfig = await tenantService.fetchTenantConfig(tenantDomain);
361
+ // Step 2: Authenticate with Cognito using USER_PASSWORD_AUTH
362
+ if (logger)
363
+ logger('Authenticating with Cognito...');
364
+ const cognitoClient = new CognitoIdentityProviderClient({ region: tenantConfig.region });
365
+ let authResult;
366
+ try {
367
+ const initiateAuthResponse = await cognitoClient.send(new InitiateAuthCommand({
368
+ AuthFlow: 'USER_PASSWORD_AUTH',
369
+ AuthParameters: {
370
+ PASSWORD: password,
371
+ USERNAME: username,
372
+ },
373
+ ClientId: tenantConfig.cognitoClientId,
374
+ }));
375
+ // Handle NEW_PASSWORD_REQUIRED challenge (first login with temp password)
376
+ if (initiateAuthResponse.ChallengeName === 'NEW_PASSWORD_REQUIRED') {
377
+ throw new Error('This CI account requires a password change on first login.\n' +
378
+ 'Please log in interactively once with: hd auth login --domain ' + tenantDomain + '\n' +
379
+ 'Or create a new CI account with: hd ci account create');
380
+ }
381
+ authResult = initiateAuthResponse.AuthenticationResult;
382
+ }
383
+ catch (error) {
384
+ const err = error;
385
+ if (err.name === 'NotAuthorizedException') {
386
+ throw new Error('Invalid token. Check your HD_TOKEN environment variable.');
387
+ }
388
+ if (err.name === 'UserNotFoundException') {
389
+ throw new Error('CI token not found or revoked. Create a new one with: hd ci account create');
390
+ }
391
+ throw error;
392
+ }
393
+ if (!authResult || !authResult.IdToken) {
394
+ throw new Error('Authentication failed: No tokens received from Cognito');
395
+ }
396
+ if (logger)
397
+ logger('Authentication successful!');
398
+ // Step 3: Get AWS credentials from Cognito Identity Pool
399
+ if (logger)
400
+ logger('Obtaining AWS credentials...');
401
+ const awsCredentials = await getAWSCredentials(tenantConfig, authResult.IdToken);
402
+ // Step 4: Save credentials
403
+ if (logger)
404
+ logger('Saving credentials...');
405
+ saveCredentials({
406
+ access_token: authResult.AccessToken || '',
407
+ apiUrl: tenantConfig.apiUrl,
408
+ awsCredentials,
409
+ cognitoConfig: {
410
+ clientId: tenantConfig.cognitoClientId,
411
+ domain: tenantConfig.cognitoDomain,
412
+ identityPoolId: tenantConfig.cognitoIdentityPoolId,
413
+ userPoolId: tenantConfig.cognitoUserPoolId,
414
+ },
415
+ expires_in: authResult.ExpiresIn || 3600,
416
+ id_token: authResult.IdToken,
417
+ obtainedAt: new Date().toISOString(),
418
+ refresh_token: authResult.RefreshToken || '',
419
+ region: tenantConfig.region,
420
+ tenantDomain: tenantConfig.tenantDomain,
421
+ tenantId: tenantConfig.tenantId,
422
+ token_type: authResult.TokenType || 'Bearer',
423
+ }, tenantConfig.tenantDomain);
424
+ return { success: true };
425
+ }
426
+ catch (error) {
427
+ const errorMessage = error instanceof Error ? error.message : String(error);
428
+ return { error: errorMessage, success: false };
429
+ }
430
+ }
431
+ /**
432
+ * Check if running in a CI environment
433
+ */
434
+ export function isCI() {
435
+ return !!(process.env.CI ||
436
+ process.env.GITHUB_ACTIONS ||
437
+ process.env.GITLAB_CI ||
438
+ process.env.JENKINS_URL ||
439
+ process.env.CIRCLECI ||
440
+ process.env.BUILDKITE ||
441
+ process.env.TRAVIS ||
442
+ process.env.CODEBUILD_BUILD_ID);
443
+ }
444
+ /**
445
+ * Decode a CI token into username and password
446
+ * Token format: hd_sk_{base64url(username:password)}
447
+ */
448
+ export function decodeToken(token) {
449
+ if (!token.startsWith('hd_sk_')) {
450
+ return null;
451
+ }
452
+ try {
453
+ const encoded = token.slice(6); // Remove 'hd_sk_' prefix
454
+ const decoded = Buffer.from(encoded, 'base64url').toString('utf-8');
455
+ const colonIndex = decoded.indexOf(':');
456
+ if (colonIndex === -1) {
457
+ return null;
458
+ }
459
+ return {
460
+ password: decoded.slice(colonIndex + 1),
461
+ username: decoded.slice(0, colonIndex),
462
+ };
463
+ }
464
+ catch {
465
+ return null;
466
+ }
467
+ }
468
+ /**
469
+ * Get CI credentials from HD_TOKEN environment variable
470
+ */
471
+ export function getCICredentials() {
472
+ const token = process.env.HD_TOKEN;
473
+ if (!token) {
474
+ return null;
475
+ }
476
+ return decodeToken(token);
477
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Options for executing the Git connect flow
3
+ */
4
+ export interface GitConnectOptions {
5
+ callbackPort?: number;
6
+ logger?: (message: string) => void;
7
+ provider?: 'github' | 'gitlab';
8
+ timeout?: number;
9
+ }
10
+ /**
11
+ * Git installation info returned from API
12
+ */
13
+ export interface GitInstallationInfo {
14
+ accountLogin?: string;
15
+ gitlabUsername?: string;
16
+ provider: 'github' | 'gitlab';
17
+ }
18
+ /**
19
+ * Result of the Git connect flow
20
+ */
21
+ export interface GitConnectResult {
22
+ accountName?: string;
23
+ error?: string;
24
+ installations?: GitInstallationInfo[];
25
+ provider?: 'github' | 'gitlab';
26
+ skipped?: boolean;
27
+ success: boolean;
28
+ }
29
+ /**
30
+ * Internal callback server result
31
+ */
32
+ interface CallbackResult {
33
+ error?: string;
34
+ success: boolean;
35
+ }
36
+ /**
37
+ * Prompt user to select a Git provider
38
+ *
39
+ * @param includeSkip - Whether to include a "Skip for now" option
40
+ * @returns Selected provider or 'skip'
41
+ */
42
+ export declare function promptGitProvider(includeSkip?: boolean): Promise<'github' | 'gitlab' | 'skip'>;
43
+ /**
44
+ * Stop the callback server if running
45
+ */
46
+ export declare function stopCallbackServer(): void;
47
+ /**
48
+ * Start local HTTP server to receive OAuth callback
49
+ *
50
+ * @param expectedState - State parameter to validate against
51
+ * @param port - Port to listen on (default: 8765)
52
+ * @param timeout - Timeout in milliseconds (default: 5 minutes)
53
+ * @param logger - Optional logging function
54
+ * @returns Promise resolving with callback result
55
+ */
56
+ export declare function waitForCallback(expectedState: string, port?: number, timeout?: number, logger?: (message: string) => void): Promise<CallbackResult>;
57
+ /**
58
+ * Execute the Git provider OAuth connection flow
59
+ *
60
+ * This function handles:
61
+ * 1. Provider selection (if not specified)
62
+ * 2. OAuth initiation via API
63
+ * 3. Starting local callback server
64
+ * 4. Opening browser for authorization
65
+ * 5. Waiting for callback with timeout
66
+ * 6. Fetching and returning connected installations
67
+ *
68
+ * @param options - Configuration options for the Git connect flow
69
+ * @returns GitConnectResult indicating success or failure
70
+ */
71
+ export declare function executeGitConnect(options?: GitConnectOptions): Promise<GitConnectResult>;
72
+ export {};