@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.
- package/README.md +1598 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +3 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/account/add.d.ts +16 -0
- package/dist/commands/account/add.js +185 -0
- package/dist/commands/account/list.d.ts +6 -0
- package/dist/commands/account/list.js +37 -0
- package/dist/commands/account/remove.d.ts +11 -0
- package/dist/commands/account/remove.js +57 -0
- package/dist/commands/auth/login.d.ts +16 -0
- package/dist/commands/auth/login.js +178 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.js +39 -0
- package/dist/commands/auth/refresh.d.ts +6 -0
- package/dist/commands/auth/refresh.js +66 -0
- package/dist/commands/auth/status.d.ts +6 -0
- package/dist/commands/auth/status.js +63 -0
- package/dist/commands/ci/account/create.d.ts +16 -0
- package/dist/commands/ci/account/create.js +158 -0
- package/dist/commands/ci/account/delete.d.ts +14 -0
- package/dist/commands/ci/account/delete.js +88 -0
- package/dist/commands/ci/account/list.d.ts +10 -0
- package/dist/commands/ci/account/list.js +65 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +37 -0
- package/dist/commands/config/set.d.ts +10 -0
- package/dist/commands/config/set.js +48 -0
- package/dist/commands/config/show.d.ts +6 -0
- package/dist/commands/config/show.js +10 -0
- package/dist/commands/deployment/create.d.ts +30 -0
- package/dist/commands/deployment/create.js +188 -0
- package/dist/commands/deployment/get.d.ts +13 -0
- package/dist/commands/deployment/get.js +101 -0
- package/dist/commands/deployment/launch.d.ts +15 -0
- package/dist/commands/deployment/launch.js +105 -0
- package/dist/commands/deployment/list.d.ts +11 -0
- package/dist/commands/deployment/list.js +91 -0
- package/dist/commands/domain/current.d.ts +6 -0
- package/dist/commands/domain/current.js +18 -0
- package/dist/commands/domain/list.d.ts +6 -0
- package/dist/commands/domain/list.js +42 -0
- package/dist/commands/domain/switch.d.ts +9 -0
- package/dist/commands/domain/switch.js +40 -0
- package/dist/commands/example.d.ts +13 -0
- package/dist/commands/example.js +24 -0
- package/dist/commands/git/connect.d.ts +10 -0
- package/dist/commands/git/connect.js +56 -0
- package/dist/commands/git/disconnect.d.ts +11 -0
- package/dist/commands/git/disconnect.js +93 -0
- package/dist/commands/git/list.d.ts +10 -0
- package/dist/commands/git/list.js +53 -0
- package/dist/commands/git/sync.d.ts +18 -0
- package/dist/commands/git/sync.js +235 -0
- package/dist/commands/init.d.ts +188 -0
- package/dist/commands/init.js +817 -0
- package/dist/commands/jira/connect.d.ts +9 -0
- package/dist/commands/jira/connect.js +141 -0
- package/dist/commands/jira/status.d.ts +9 -0
- package/dist/commands/jira/status.js +118 -0
- package/dist/commands/module/analyze.d.ts +29 -0
- package/dist/commands/module/analyze.js +201 -0
- package/dist/commands/module/create.d.ts +42 -0
- package/dist/commands/module/create.js +498 -0
- package/dist/commands/module/destroy.d.ts +11 -0
- package/dist/commands/module/destroy.js +77 -0
- package/dist/commands/module/get.d.ts +10 -0
- package/dist/commands/module/get.js +43 -0
- package/dist/commands/module/link.d.ts +15 -0
- package/dist/commands/module/link.js +175 -0
- package/dist/commands/module/list.d.ts +9 -0
- package/dist/commands/module/list.js +51 -0
- package/dist/commands/module/reanalyze.d.ts +30 -0
- package/dist/commands/module/reanalyze.js +206 -0
- package/dist/commands/module/update.d.ts +27 -0
- package/dist/commands/module/update.js +102 -0
- package/dist/commands/parameter/add.d.ts +15 -0
- package/dist/commands/parameter/add.js +99 -0
- package/dist/commands/parameter/backfill.d.ts +12 -0
- package/dist/commands/parameter/backfill.js +113 -0
- package/dist/commands/parameter/clear.d.ts +14 -0
- package/dist/commands/parameter/clear.js +95 -0
- package/dist/commands/parameter/list.d.ts +14 -0
- package/dist/commands/parameter/list.js +92 -0
- package/dist/commands/parameter/pull.d.ts +14 -0
- package/dist/commands/parameter/pull.js +124 -0
- package/dist/commands/parameter/remove.d.ts +15 -0
- package/dist/commands/parameter/remove.js +90 -0
- package/dist/commands/parameter/sync.d.ts +14 -0
- package/dist/commands/parameter/sync.js +153 -0
- package/dist/commands/parameter/update.d.ts +15 -0
- package/dist/commands/parameter/update.js +100 -0
- package/dist/commands/stage/create.d.ts +28 -0
- package/dist/commands/stage/create.js +312 -0
- package/dist/commands/stage/list.d.ts +9 -0
- package/dist/commands/stage/list.js +63 -0
- package/dist/commands/test-api.d.ts +9 -0
- package/dist/commands/test-api.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/auth-service.d.ts +84 -0
- package/dist/services/auth-service.js +240 -0
- package/dist/services/git.d.ts +46 -0
- package/dist/services/git.js +409 -0
- package/dist/services/hyperdrive-sigv4.d.ts +449 -0
- package/dist/services/hyperdrive-sigv4.js +375 -0
- package/dist/services/hyperdrive.d.ts +87 -0
- package/dist/services/hyperdrive.js +108 -0
- package/dist/services/log-tailer.d.ts +95 -0
- package/dist/services/log-tailer.js +242 -0
- package/dist/services/tenant-service.d.ts +106 -0
- package/dist/services/tenant-service.js +332 -0
- package/dist/utils/account-flow.d.ts +74 -0
- package/dist/utils/account-flow.js +228 -0
- package/dist/utils/auth-flow.d.ts +146 -0
- package/dist/utils/auth-flow.js +477 -0
- package/dist/utils/git-flow.d.ts +72 -0
- package/dist/utils/git-flow.js +232 -0
- package/dist/utils/jira-flow.d.ts +71 -0
- package/dist/utils/jira-flow.js +120 -0
- package/dist/utils/summary-display.d.ts +59 -0
- package/dist/utils/summary-display.js +140 -0
- package/dist/utils/validation.d.ts +15 -0
- package/dist/utils/validation.js +32 -0
- package/oclif.manifest.json +2819 -0
- 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 {};
|