@hyperdrive.bot/cli 1.0.5 → 1.0.7

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 (67) hide show
  1. package/README.md +169 -63
  2. package/dist/commands/account/add.d.ts +6 -6
  3. package/dist/commands/account/remove.d.ts +3 -3
  4. package/dist/commands/auth/login.d.ts +4 -4
  5. package/dist/commands/auth/login.js +1 -0
  6. package/dist/commands/ci/account/create.d.ts +7 -6
  7. package/dist/commands/ci/account/create.js +49 -3
  8. package/dist/commands/ci/account/delete.d.ts +3 -3
  9. package/dist/commands/ci/account/list.d.ts +2 -2
  10. package/dist/commands/config/get.d.ts +1 -1
  11. package/dist/commands/config/set.d.ts +2 -2
  12. package/dist/commands/deployment/create.d.ts +10 -10
  13. package/dist/commands/deployment/get.d.ts +4 -4
  14. package/dist/commands/deployment/launch.d.ts +6 -6
  15. package/dist/commands/deployment/list.d.ts +3 -3
  16. package/dist/commands/deployment/list.js +17 -17
  17. package/dist/commands/domain/switch.d.ts +1 -1
  18. package/dist/commands/example.d.ts +3 -3
  19. package/dist/commands/git/connect.d.ts +2 -2
  20. package/dist/commands/git/disconnect.d.ts +3 -3
  21. package/dist/commands/git/list.d.ts +2 -2
  22. package/dist/commands/git/sync.d.ts +7 -7
  23. package/dist/commands/git/sync.js +24 -23
  24. package/dist/commands/jira/connect.d.ts +1 -1
  25. package/dist/commands/jira/status.d.ts +1 -1
  26. package/dist/commands/module/analyze.d.ts +5 -5
  27. package/dist/commands/module/create.d.ts +17 -17
  28. package/dist/commands/module/create.js +9 -1
  29. package/dist/commands/module/destroy.d.ts +3 -3
  30. package/dist/commands/module/get.d.ts +2 -2
  31. package/dist/commands/module/link.d.ts +4 -4
  32. package/dist/commands/module/list.d.ts +1 -1
  33. package/dist/commands/module/list.js +12 -11
  34. package/dist/commands/module/reanalyze.d.ts +6 -6
  35. package/dist/commands/module/update.d.ts +19 -19
  36. package/dist/commands/parameter/add.d.ts +7 -7
  37. package/dist/commands/parameter/backfill.d.ts +4 -4
  38. package/dist/commands/parameter/backfill.js +4 -3
  39. package/dist/commands/parameter/clear.d.ts +6 -6
  40. package/dist/commands/parameter/list.d.ts +6 -6
  41. package/dist/commands/parameter/list.js +4 -3
  42. package/dist/commands/parameter/pull.d.ts +6 -6
  43. package/dist/commands/parameter/remove.d.ts +7 -7
  44. package/dist/commands/parameter/sync.d.ts +6 -6
  45. package/dist/commands/parameter/update.d.ts +7 -7
  46. package/dist/commands/stage/access.d.ts +15 -0
  47. package/dist/commands/stage/access.js +130 -0
  48. package/dist/commands/stage/create.d.ts +11 -11
  49. package/dist/commands/stage/list.d.ts +1 -1
  50. package/dist/commands/stage/list.js +21 -20
  51. package/dist/commands/stage/revoke.d.ts +18 -0
  52. package/dist/commands/stage/revoke.js +171 -0
  53. package/dist/commands/stage/share.d.ts +23 -0
  54. package/dist/commands/stage/share.js +292 -0
  55. package/dist/commands/test-api.d.ts +1 -1
  56. package/dist/services/auth-service.d.ts +15 -82
  57. package/dist/services/auth-service.js +24 -237
  58. package/dist/services/hyperdrive-sigv4.d.ts +37 -24
  59. package/dist/services/hyperdrive-sigv4.js +62 -193
  60. package/dist/services/tenant-service.d.ts +6 -0
  61. package/dist/services/tenant-service.js +13 -0
  62. package/dist/utils/auth-flow.d.ts +1 -0
  63. package/dist/utils/auth-flow.js +2 -0
  64. package/dist/utils/table.d.ts +17 -0
  65. package/dist/utils/table.js +41 -0
  66. package/oclif.manifest.json +309 -81
  67. package/package.json +55 -15
@@ -0,0 +1,292 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ const ROLE_CHOICES = [
6
+ {
7
+ name: 'Deployer - Can deploy to this stage',
8
+ short: 'Deployer',
9
+ value: 'deployer',
10
+ },
11
+ {
12
+ name: 'Viewer - Can view stage details and deployments',
13
+ short: 'Viewer',
14
+ value: 'viewer',
15
+ },
16
+ {
17
+ name: 'Manager - Full control including access management',
18
+ short: 'Manager',
19
+ value: 'manager',
20
+ },
21
+ ];
22
+ export default class StageShare extends Command {
23
+ static args = {
24
+ stage: Args.string({
25
+ description: 'Stage name to share access to',
26
+ required: false,
27
+ }),
28
+ };
29
+ static description = 'Grant access to a stage for users or CI accounts';
30
+ static examples = [
31
+ // Interactive
32
+ '<%= config.bin %> <%= command.id %>',
33
+ '<%= config.bin %> <%= command.id %> develop',
34
+ // Non-interactive
35
+ '<%= config.bin %> <%= command.id %> develop --user maria@company.com --role deployer',
36
+ '<%= config.bin %> <%= command.id %> develop --ci github-actions --role deployer',
37
+ '<%= config.bin %> <%= command.id %> develop --group "CI Accounts" --role deployer',
38
+ '<%= config.bin %> <%= command.id %> staging production --user dev@company.com --role viewer',
39
+ ];
40
+ static flags = {
41
+ ci: Flags.string({
42
+ char: 'c',
43
+ description: 'CI account name or ID to grant access to',
44
+ exclusive: ['user', 'group'],
45
+ }),
46
+ domain: Flags.string({
47
+ char: 'd',
48
+ description: 'Tenant domain (for multi-domain setups)',
49
+ }),
50
+ group: Flags.string({
51
+ char: 'g',
52
+ description: 'Group ID or name to grant access to',
53
+ exclusive: ['user', 'ci'],
54
+ }),
55
+ role: Flags.string({
56
+ char: 'r',
57
+ description: 'Role to grant',
58
+ options: ['viewer', 'deployer', 'manager'],
59
+ }),
60
+ user: Flags.string({
61
+ char: 'u',
62
+ description: 'User email to grant access to',
63
+ exclusive: ['ci', 'group'],
64
+ }),
65
+ yes: Flags.boolean({
66
+ char: 'y',
67
+ default: false,
68
+ description: 'Skip confirmation for production stages',
69
+ }),
70
+ };
71
+ static strict = false; // Allow multiple stage args
72
+ async run() {
73
+ const { argv, flags } = await this.parse(StageShare);
74
+ const stageArgs = argv;
75
+ this.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
76
+ this.log(chalk.cyan('🔐 Share Stage Access'));
77
+ this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
78
+ this.log('');
79
+ const service = new HyperdriveSigV4Service(flags.domain);
80
+ // Step 1: Resolve stage(s)
81
+ let stages;
82
+ if (stageArgs.length > 0) {
83
+ stages = stageArgs;
84
+ }
85
+ else {
86
+ stages = await this.promptForStages(service);
87
+ }
88
+ // Step 2: Resolve target (user, CI, or group)
89
+ let targetType;
90
+ let targetId;
91
+ let displayName;
92
+ if (flags.group) {
93
+ // Find group by ID or name
94
+ const group = await this.findGroup(service, flags.group);
95
+ if (!group) {
96
+ this.error(`Group '${flags.group}' not found`);
97
+ }
98
+ targetType = 'group';
99
+ targetId = group.groupId;
100
+ displayName = `Group: ${group.name}`;
101
+ }
102
+ else if (flags.user) {
103
+ targetType = 'user';
104
+ targetId = flags.user;
105
+ displayName = flags.user;
106
+ }
107
+ else if (flags.ci) {
108
+ // For CI accounts, we need to look up the Cognito sub
109
+ const ciAccount = await this.findCIAccount(service, flags.ci);
110
+ if (!ciAccount) {
111
+ this.error(`CI account '${flags.ci}' not found`);
112
+ }
113
+ targetType = 'user';
114
+ // CI accounts use cognitoUsername but we need the sub for Zanzibar
115
+ // For now, we'll use the cognitoUsername and the backend will resolve it
116
+ targetId = ciAccount.cognitoUsername;
117
+ displayName = `CI: ${ciAccount.name}`;
118
+ }
119
+ else {
120
+ const target = await this.promptForTarget(service);
121
+ targetType = target.type;
122
+ targetId = target.id;
123
+ displayName = target.displayName;
124
+ }
125
+ // Step 3: Resolve role
126
+ const role = flags.role || await this.promptForRole();
127
+ // Step 4: Confirm for production stages
128
+ const prodStages = stages.filter(s => s.toLowerCase().includes('prod'));
129
+ if (prodStages.length > 0 && !flags.yes) {
130
+ const { confirmed } = await inquirer.prompt([{
131
+ default: false,
132
+ message: chalk.yellow(`⚠️ You're granting access to production stage(s): ${prodStages.join(', ')}. Continue?`),
133
+ name: 'confirmed',
134
+ type: 'confirm',
135
+ }]);
136
+ if (!confirmed) {
137
+ this.log(chalk.gray('Cancelled.'));
138
+ return;
139
+ }
140
+ }
141
+ // Step 5: Grant access to each stage
142
+ this.log('');
143
+ for (const stage of stages) {
144
+ try {
145
+ this.log(chalk.blue(`🔄 Granting ${role} access to '${stage}' for ${displayName}...`));
146
+ await service.stageAccessGrant(stage, [{
147
+ role,
148
+ targetId,
149
+ targetType,
150
+ }]);
151
+ this.log(chalk.green(`✅ Granted '${role}' access to ${displayName} on '${stage}'`));
152
+ }
153
+ catch (error) {
154
+ const err = error;
155
+ const message = err.response?.data?.message || err.message;
156
+ this.log(chalk.red(`❌ Failed to grant access to '${stage}': ${message}`));
157
+ }
158
+ }
159
+ this.log('');
160
+ }
161
+ async findCIAccount(service, nameOrId) {
162
+ try {
163
+ const accounts = await service.ciAccountList();
164
+ return accounts.find(a => a.name.toLowerCase() === nameOrId.toLowerCase() ||
165
+ a.accountId === nameOrId ||
166
+ a.cognitoUsername.includes(nameOrId)) || null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ async findGroup(service, nameOrId) {
173
+ try {
174
+ const groups = await service.groupList();
175
+ return groups.find(g => g.groupId === nameOrId ||
176
+ g.name.toLowerCase() === nameOrId.toLowerCase()) || null;
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ async promptForRole() {
183
+ const { role } = await inquirer.prompt([{
184
+ choices: ROLE_CHOICES,
185
+ message: chalk.yellow('Select role to grant:'),
186
+ name: 'role',
187
+ type: 'list',
188
+ }]);
189
+ return role;
190
+ }
191
+ async promptForStages(service) {
192
+ this.log(chalk.gray(' Fetching stages...'));
193
+ const stagesData = await service.stageList();
194
+ if (!stagesData || stagesData.length === 0) {
195
+ this.error('No stages found. Create a stage first with: hd stage create');
196
+ }
197
+ const stageChoices = stagesData.map(s => ({
198
+ name: s.production
199
+ ? `${s.name} ${chalk.red('(PROD)')}`
200
+ : `${s.name} ${chalk.blue('(dev)')}`,
201
+ value: s.slug || s.name,
202
+ }));
203
+ const { stages } = await inquirer.prompt([{
204
+ choices: stageChoices,
205
+ message: chalk.yellow('Select stage(s) to share:'),
206
+ name: 'stages',
207
+ type: 'checkbox',
208
+ validate: (input) => input.length > 0 || 'Select at least one stage',
209
+ }]);
210
+ return stages;
211
+ }
212
+ async promptForTarget(service) {
213
+ // First ask what type of target
214
+ const { targetType } = await inquirer.prompt([{
215
+ choices: [
216
+ { name: 'User (by email)', value: 'user' },
217
+ { name: 'CI Account', value: 'ci' },
218
+ { name: 'Group', value: 'group' },
219
+ ],
220
+ message: chalk.yellow('Grant access to:'),
221
+ name: 'targetType',
222
+ type: 'list',
223
+ }]);
224
+ if (targetType === 'group') {
225
+ // Fetch groups and let user select
226
+ this.log(chalk.gray(' Fetching groups...'));
227
+ const groups = await service.groupList();
228
+ if (!groups || groups.length === 0) {
229
+ this.error('No groups found. Create a group first or use user/CI account.');
230
+ }
231
+ const groupChoices = groups.map(g => ({
232
+ name: g.isSystemGroup
233
+ ? `${g.name} ${chalk.cyan('(system)')}`
234
+ : g.name,
235
+ value: { groupId: g.groupId, name: g.name },
236
+ }));
237
+ const { group } = await inquirer.prompt([{
238
+ choices: groupChoices,
239
+ message: chalk.yellow('Select group:'),
240
+ name: 'group',
241
+ type: 'list',
242
+ }]);
243
+ return {
244
+ displayName: `Group: ${group.name}`,
245
+ id: group.groupId,
246
+ type: 'group',
247
+ };
248
+ }
249
+ if (targetType === 'ci') {
250
+ // Fetch CI accounts and let user select
251
+ this.log(chalk.gray(' Fetching CI accounts...'));
252
+ const ciAccounts = await service.ciAccountList();
253
+ if (!ciAccounts || ciAccounts.length === 0) {
254
+ this.error('No CI accounts found. Create one first with: hd ci account create');
255
+ }
256
+ const ciChoices = ciAccounts.map(a => ({
257
+ name: `${a.name} (${a.cognitoUsername.split('@')[0]})`,
258
+ value: {
259
+ cognitoUsername: a.cognitoUsername,
260
+ name: a.name,
261
+ },
262
+ }));
263
+ const { ciAccount } = await inquirer.prompt([{
264
+ choices: ciChoices,
265
+ message: chalk.yellow('Select CI account:'),
266
+ name: 'ciAccount',
267
+ type: 'list',
268
+ }]);
269
+ return {
270
+ displayName: `CI: ${ciAccount.name}`,
271
+ id: ciAccount.cognitoUsername,
272
+ type: 'user',
273
+ };
274
+ }
275
+ // For users, prompt for email
276
+ const { email } = await inquirer.prompt([{
277
+ message: chalk.yellow('Enter user email:'),
278
+ name: 'email',
279
+ validate: (input) => {
280
+ if (!input || !input.includes('@')) {
281
+ return 'Please enter a valid email address';
282
+ }
283
+ return true;
284
+ },
285
+ }]);
286
+ return {
287
+ displayName: email,
288
+ id: email,
289
+ type: 'user',
290
+ };
291
+ }
292
+ }
@@ -3,7 +3,7 @@ export default class TestApi extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  }
@@ -1,84 +1,17 @@
1
- export interface CognitoTokens {
2
- access_token: string;
3
- expires_in: number;
4
- id_token: string;
5
- refresh_token: string;
6
- token_type: string;
7
- }
8
- export interface AWSCredentials {
9
- accessKeyId: string;
10
- expiration: Date;
11
- secretAccessKey: string;
12
- sessionToken: string;
13
- }
14
- export interface CognitoConfig {
15
- clientId: string;
16
- domain: string;
17
- identityPoolId: string;
18
- userPoolId: string;
19
- }
20
- export interface StoredCredentials extends CognitoTokens {
21
- apiUrl: string;
22
- awsCredentials: AWSCredentials;
23
- cognitoConfig?: CognitoConfig;
24
- obtainedAt: string;
25
- region: string;
26
- tenantDomain: string;
27
- tenantId: string;
28
- }
29
- export declare class AuthService {
30
- private readonly cognitoConfig;
31
- private readonly credDir;
32
- private readonly credPath;
33
- private readonly domain?;
1
+ /**
2
+ * Hyperdrive Authentication Service
3
+ *
4
+ * Re-exports from @devsquad/cli-auth with hyperdrive-specific configuration.
5
+ */
6
+ import { AuthService as BaseAuthService, type AuthServiceConfig, type AWSCredentials, type CognitoConfig, type CognitoTokens, type StoredCredentials } from '@hyperdrive.bot/cli-auth';
7
+ export type { AWSCredentials, CognitoConfig, CognitoTokens, StoredCredentials };
8
+ declare const HYPERDRIVE_AUTH_CONFIG: AuthServiceConfig;
9
+ /**
10
+ * Hyperdrive-specific AuthService
11
+ *
12
+ * Wrapper around @devsquad/cli-auth with hyperdrive defaults.
13
+ */
14
+ export declare class AuthService extends BaseAuthService {
34
15
  constructor(domain?: string);
35
- /**
36
- * Clear stored credentials
37
- */
38
- clearCredentials(): void;
39
- /**
40
- * Ensure credentials are valid, refresh if needed
41
- * Returns valid credentials or throws error
42
- */
43
- ensureValidCredentials(): Promise<StoredCredentials>;
44
- /**
45
- * Get AWS credentials from Cognito Identity Pool using ID token
46
- */
47
- getAWSCredentials(idToken: string, region: string, cognitoConfig: CognitoConfig): Promise<AWSCredentials>;
48
- /**
49
- * Get all domains with stored credentials
50
- */
51
- getCredentialDomains(): string[];
52
- /**
53
- * Check if credentials are expired
54
- */
55
- isExpired(credentials: StoredCredentials): boolean;
56
- /**
57
- * Load stored credentials from domain-specific path
58
- *
59
- * Always uses domain-specific credentials (credentials.<domain>)
60
- * Domain is either explicitly specified or uses default domain
61
- */
62
- loadCredentials(): StoredCredentials | null;
63
- /**
64
- * Check if credentials need refresh
65
- * Returns true if AWS credentials expire in less than 5 minutes
66
- */
67
- needsRefresh(credentials: StoredCredentials): boolean;
68
- /**
69
- * Refresh Cognito tokens using refresh token
70
- */
71
- refreshTokens(refreshToken: string, cognitoConfig: CognitoConfig): Promise<CognitoTokens>;
72
- /**
73
- * Save credentials to ~/.hyperdrive/credentials
74
- */
75
- saveCredentials(credentials: StoredCredentials): void;
76
- /**
77
- * Get the credentials file path (always domain-specific)
78
- */
79
- private getCredentialsPath;
80
- /**
81
- * Get default domain from TenantService (avoid circular dependency by reading file directly)
82
- */
83
- private getDefaultDomain;
84
16
  }
17
+ export { HYPERDRIVE_AUTH_CONFIG };
@@ -1,240 +1,27 @@
1
- import { CognitoIdentityClient, GetCredentialsForIdentityCommand, GetIdCommand } from '@aws-sdk/client-cognito-identity';
2
- import axios from 'axios';
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
4
- import { homedir } from 'os';
5
- import { join } from 'path';
6
- export class AuthService {
7
- cognitoConfig = {
8
- clientId: process.env.HYPERDRIVE_COGNITO_CLIENT_ID || '',
9
- domain: process.env.HYPERDRIVE_COGNITO_DOMAIN || 'hyperdrive.auth.us-east-1.amazoncognito.com',
10
- identityPoolId: process.env.HYPERDRIVE_COGNITO_IDENTITY_POOL_ID || '',
11
- region: process.env.HYPERDRIVE_AWS_REGION || 'us-east-1',
12
- userPoolId: process.env.HYPERDRIVE_COGNITO_USER_POOL_ID || '',
13
- };
14
- credDir;
15
- credPath;
16
- domain;
1
+ /**
2
+ * Hyperdrive Authentication Service
3
+ *
4
+ * Re-exports from @devsquad/cli-auth with hyperdrive-specific configuration.
5
+ */
6
+ import { AuthService as BaseAuthService, } from '@hyperdrive.bot/cli-auth';
7
+ // Hyperdrive-specific configuration
8
+ const HYPERDRIVE_AUTH_CONFIG = {
9
+ appName: 'hyperdrive',
10
+ defaultCognitoClientId: process.env.HYPERDRIVE_COGNITO_CLIENT_ID || '',
11
+ defaultCognitoDomain: process.env.HYPERDRIVE_COGNITO_DOMAIN || 'hyperdrive.auth.us-east-1.amazoncognito.com',
12
+ defaultIdentityPoolId: process.env.HYPERDRIVE_COGNITO_IDENTITY_POOL_ID || '',
13
+ defaultUserPoolId: process.env.HYPERDRIVE_COGNITO_USER_POOL_ID || '',
14
+ defaultRegion: process.env.HYPERDRIVE_AWS_REGION || 'us-east-1',
15
+ };
16
+ /**
17
+ * Hyperdrive-specific AuthService
18
+ *
19
+ * Wrapper around @devsquad/cli-auth with hyperdrive defaults.
20
+ */
21
+ export class AuthService extends BaseAuthService {
17
22
  constructor(domain) {
18
- this.credDir = join(homedir(), '.hyperdrive');
19
- this.domain = domain;
20
- this.credPath = this.getCredentialsPath();
21
- }
22
- /**
23
- * Clear stored credentials
24
- */
25
- clearCredentials() {
26
- if (existsSync(this.credPath)) {
27
- unlinkSync(this.credPath);
28
- }
29
- }
30
- /**
31
- * Ensure credentials are valid, refresh if needed
32
- * Returns valid credentials or throws error
33
- */
34
- async ensureValidCredentials() {
35
- const credentials = this.loadCredentials();
36
- if (!credentials) {
37
- throw new Error('Not authenticated. Please run "hd auth login" first.');
38
- }
39
- // Auto-refresh if needed
40
- if (this.needsRefresh(credentials)) {
41
- if (!credentials.cognitoConfig) {
42
- throw new Error('Cognito configuration not found. Please run "hd auth login" again.');
43
- }
44
- console.log('⏳ Credentials expiring soon, refreshing...');
45
- const newTokens = await this.refreshTokens(credentials.refresh_token, credentials.cognitoConfig);
46
- const newAwsCredentials = await this.getAWSCredentials(newTokens.id_token, credentials.region, credentials.cognitoConfig);
47
- const updatedCredentials = {
48
- ...newTokens,
49
- apiUrl: credentials.apiUrl,
50
- awsCredentials: newAwsCredentials,
51
- cognitoConfig: credentials.cognitoConfig,
52
- obtainedAt: new Date().toISOString(),
53
- region: credentials.region,
54
- tenantDomain: credentials.tenantDomain,
55
- tenantId: credentials.tenantId,
56
- };
57
- this.saveCredentials(updatedCredentials);
58
- console.log('✅ Credentials refreshed automatically');
59
- return updatedCredentials;
60
- }
61
- return credentials;
62
- }
63
- /**
64
- * Get AWS credentials from Cognito Identity Pool using ID token
65
- */
66
- async getAWSCredentials(idToken, region, cognitoConfig) {
67
- const client = new CognitoIdentityClient({ region });
68
- try {
69
- // Step 1: Get Identity ID
70
- const getIdResponse = await client.send(new GetIdCommand({
71
- IdentityPoolId: cognitoConfig.identityPoolId,
72
- Logins: {
73
- [`cognito-idp.${region}.amazonaws.com/${cognitoConfig.userPoolId}`]: idToken,
74
- },
75
- }));
76
- if (!getIdResponse.IdentityId) {
77
- throw new Error('Failed to get Identity ID from Cognito');
78
- }
79
- // Step 2: Get temporary AWS credentials
80
- const getCredentialsResponse = await client.send(new GetCredentialsForIdentityCommand({
81
- IdentityId: getIdResponse.IdentityId,
82
- Logins: {
83
- [`cognito-idp.${region}.amazonaws.com/${cognitoConfig.userPoolId}`]: idToken,
84
- },
85
- }));
86
- if (!getCredentialsResponse.Credentials) {
87
- throw new Error('Failed to get AWS credentials from Cognito');
88
- }
89
- const { AccessKeyId, Expiration, SecretKey, SessionToken } = getCredentialsResponse.Credentials;
90
- if (!AccessKeyId || !SecretKey || !SessionToken || !Expiration) {
91
- throw new Error('Incomplete AWS credentials received from Cognito');
92
- }
93
- return {
94
- accessKeyId: AccessKeyId,
95
- expiration: Expiration,
96
- secretAccessKey: SecretKey,
97
- sessionToken: SessionToken,
98
- };
99
- }
100
- catch (error) {
101
- if (error instanceof Error) {
102
- throw new Error(`Failed to obtain AWS credentials: ${error.message}`);
103
- }
104
- throw error;
105
- }
106
- }
107
- /**
108
- * Get all domains with stored credentials
109
- */
110
- getCredentialDomains() {
111
- try {
112
- if (!existsSync(this.credDir)) {
113
- return [];
114
- }
115
- const files = require('fs').readdirSync(this.credDir);
116
- const domains = [];
117
- for (const file of files) {
118
- // Match files like "credentials.example.com"
119
- const match = file.match(/^credentials\.(.+)$/);
120
- if (match) {
121
- domains.push(match[1]);
122
- }
123
- }
124
- return domains;
125
- }
126
- catch (error) {
127
- return [];
128
- }
129
- }
130
- /**
131
- * Check if credentials are expired
132
- */
133
- isExpired(credentials) {
134
- const expiration = new Date(credentials.awsCredentials.expiration);
135
- return new Date() >= expiration;
136
- }
137
- /**
138
- * Load stored credentials from domain-specific path
139
- *
140
- * Always uses domain-specific credentials (credentials.<domain>)
141
- * Domain is either explicitly specified or uses default domain
142
- */
143
- loadCredentials() {
144
- try {
145
- if (!existsSync(this.credPath)) {
146
- return null;
147
- }
148
- const data = readFileSync(this.credPath, 'utf8');
149
- const credentials = JSON.parse(data);
150
- // Convert expiration string back to Date object
151
- credentials.awsCredentials.expiration = new Date(credentials.awsCredentials.expiration);
152
- return credentials;
153
- }
154
- catch (error) {
155
- console.error('Failed to load credentials:', error);
156
- return null;
157
- }
158
- }
159
- /**
160
- * Check if credentials need refresh
161
- * Returns true if AWS credentials expire in less than 5 minutes
162
- */
163
- needsRefresh(credentials) {
164
- const expiration = new Date(credentials.awsCredentials.expiration);
165
- const now = new Date();
166
- const timeUntilExpiry = expiration.getTime() - now.getTime();
167
- const fiveMinutes = 5 * 60 * 1000;
168
- return timeUntilExpiry < fiveMinutes;
169
- }
170
- /**
171
- * Refresh Cognito tokens using refresh token
172
- */
173
- async refreshTokens(refreshToken, cognitoConfig) {
174
- try {
175
- const response = await axios.post(`https://${cognitoConfig.domain}/oauth2/token`, new URLSearchParams({
176
- client_id: cognitoConfig.clientId,
177
- grant_type: 'refresh_token',
178
- refresh_token: refreshToken,
179
- }), {
180
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
181
- });
182
- // Cognito doesn't return a new refresh token on refresh, so we need to keep the old one
183
- return {
184
- ...response.data,
185
- refresh_token: refreshToken, // Preserve original refresh token
186
- };
187
- }
188
- catch (error) {
189
- if (axios.isAxiosError(error)) {
190
- throw new Error(`Token refresh failed: ${error.response?.data?.error_description || error.message}`);
191
- }
192
- throw error;
193
- }
194
- }
195
- /**
196
- * Save credentials to ~/.hyperdrive/credentials
197
- */
198
- saveCredentials(credentials) {
199
- // Prevent saving test/mock credentials to production path
200
- const testPatterns = ['test-client-id', 'us-east-1_TEST', 'test-identity', 'test.auth.amazoncognito'];
201
- const credString = JSON.stringify(credentials);
202
- for (const pattern of testPatterns) {
203
- if (credString.includes(pattern)) {
204
- throw new Error(`Refusing to save credentials containing test value: "${pattern}". This looks like test data.`);
205
- }
206
- }
207
- // Ensure directory exists
208
- if (!existsSync(this.credDir)) {
209
- mkdirSync(this.credDir, { recursive: true });
210
- }
211
- // Write with secure permissions (owner read/write only)
212
- writeFileSync(this.credPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
213
- }
214
- /**
215
- * Get the credentials file path (always domain-specific)
216
- */
217
- getCredentialsPath() {
218
- const domain = this.domain || this.getDefaultDomain();
219
- if (!domain) {
220
- throw new Error('No domain specified and no default domain configured');
221
- }
222
- // Domain-specific credentials: ~/.hyperdrive/credentials.{domain}
223
- return join(this.credDir, `credentials.${domain}`);
224
- }
225
- /**
226
- * Get default domain from TenantService (avoid circular dependency by reading file directly)
227
- */
228
- getDefaultDomain() {
229
- try {
230
- const defaultDomainPath = join(this.credDir, 'default-domain');
231
- if (!existsSync(defaultDomainPath)) {
232
- return null;
233
- }
234
- return readFileSync(defaultDomainPath, 'utf8').trim();
235
- }
236
- catch (error) {
237
- return null;
238
- }
23
+ super(HYPERDRIVE_AUTH_CONFIG, domain);
239
24
  }
240
25
  }
26
+ // Export config for use by HyperdriveSigV4Service
27
+ export { HYPERDRIVE_AUTH_CONFIG };