@gugananuvem/aws-local-simulator 1.0.35 → 1.0.39

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 (149) hide show
  1. package/README.md +662 -658
  2. package/package.json +10 -3
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/index.js +26 -0
  100. package/src/services/lambda/server.js +12 -1
  101. package/src/services/lambda/simulator.js +4 -0
  102. package/src/services/opensearch/index.js +105 -0
  103. package/src/services/opensearch/models/document.js +115 -0
  104. package/src/services/opensearch/models/index.js +73 -0
  105. package/src/services/opensearch/server.js +310 -0
  106. package/src/services/opensearch/simulator.js +605 -0
  107. package/src/services/opensearch/store.js +162 -0
  108. package/src/services/organizations/control-tower-simulator.js +321 -0
  109. package/src/services/organizations/control-tower-store.js +182 -0
  110. package/src/services/organizations/iam-integration.js +413 -0
  111. package/src/services/organizations/index.js +172 -0
  112. package/src/services/organizations/models/index.js +13 -0
  113. package/src/services/organizations/models/invitation.js +48 -0
  114. package/src/services/organizations/models/organization.js +566 -0
  115. package/src/services/organizations/models/role.js +244 -0
  116. package/src/services/organizations/models/scp.js +131 -0
  117. package/src/services/organizations/server.js +736 -0
  118. package/src/services/organizations/simulator.js +593 -0
  119. package/src/services/organizations/store.js +235 -0
  120. package/src/services/organizations/vpc-integration.js +453 -0
  121. package/src/services/route53/index.js +67 -0
  122. package/src/services/route53/models/healthCheck.js +101 -0
  123. package/src/services/route53/models/hostedZone.js +37 -0
  124. package/src/services/route53/models/resourceRecord.js +106 -0
  125. package/src/services/route53/server.js +841 -0
  126. package/src/services/route53/simulator.js +350 -0
  127. package/src/services/s3/index.js +62 -0
  128. package/src/services/s3/presigned-url-generator.js +337 -0
  129. package/src/services/s3/server.js +231 -0
  130. package/src/services/s3/simulator.js +551 -4
  131. package/src/services/sns/index.js +31 -0
  132. package/src/services/sns/server.js +9 -0
  133. package/src/services/sqs/index.js +38 -0
  134. package/src/services/sqs/server.js +57 -4
  135. package/src/services/sqs/simulator.js +423 -33
  136. package/src/services/sts/simulator.js +209 -17
  137. package/src/services/vpc/README.md +459 -0
  138. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  139. package/src/services/vpc/index.js +90 -0
  140. package/src/services/vpc/models/internetGateway.js +71 -0
  141. package/src/services/vpc/models/routeTable.js +160 -0
  142. package/src/services/vpc/models/subnet.js +48 -0
  143. package/src/services/vpc/models/vpc.js +37 -0
  144. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  145. package/src/services/vpc/models/vpcGateway.js +134 -0
  146. package/src/services/vpc/server.js +836 -0
  147. package/src/services/vpc/simulator.js +613 -0
  148. package/src/utils/policy-cache.js +215 -0
  149. package/aws-config +0 -154
@@ -0,0 +1,326 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * CloudFront JSON Persistence Store
6
+ * Manages persistence of distributions and related data to JSON files
7
+ */
8
+ class CloudFrontStore {
9
+ constructor(storePath = './data/cloudfront') {
10
+ this.storePath = storePath;
11
+ this.distributionsFile = path.join(storePath, 'distributions.json');
12
+ this.invalidationsFile = path.join(storePath, 'invalidations.json');
13
+ this.keyPairsFile = path.join(storePath, 'key-pairs.json');
14
+ this.init();
15
+ }
16
+
17
+ /**
18
+ * Initialize store - create directories and files if needed
19
+ */
20
+ init() {
21
+ if (!fs.existsSync(this.storePath)) {
22
+ fs.mkdirSync(this.storePath, { recursive: true });
23
+ }
24
+
25
+ // Initialize distribution file
26
+ if (!fs.existsSync(this.distributionsFile)) {
27
+ this._writeFile(this.distributionsFile, { distributions: [] });
28
+ }
29
+
30
+ // Initialize invalidations file
31
+ if (!fs.existsSync(this.invalidationsFile)) {
32
+ this._writeFile(this.invalidationsFile, { invalidations: [] });
33
+ }
34
+
35
+ // Initialize key pairs file
36
+ if (!fs.existsSync(this.keyPairsFile)) {
37
+ this._writeFile(this.keyPairsFile, { keyPairs: [] });
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Save a distribution
43
+ * @param {object} distribution - Distribution object
44
+ */
45
+ saveDistribution(distribution) {
46
+ const data = this._readFile(this.distributionsFile);
47
+
48
+ const index = data.distributions.findIndex(d => d.id === distribution.id);
49
+ if (index !== -1) {
50
+ data.distributions[index] = distribution;
51
+ } else {
52
+ data.distributions.push(distribution);
53
+ }
54
+
55
+ this._writeFile(this.distributionsFile, data);
56
+ }
57
+
58
+ /**
59
+ * Get a distribution
60
+ * @param {string} distributionId - Distribution ID
61
+ * @returns {object|null} Distribution or null
62
+ */
63
+ getDistribution(distributionId) {
64
+ const data = this._readFile(this.distributionsFile);
65
+ return data.distributions.find(d => d.id === distributionId) || null;
66
+ }
67
+
68
+ /**
69
+ * Get all distributions
70
+ * @returns {object[]} Array of distributions
71
+ */
72
+ getAllDistributions() {
73
+ const data = this._readFile(this.distributionsFile);
74
+ return data.distributions || [];
75
+ }
76
+
77
+ /**
78
+ * Delete a distribution
79
+ * @param {string} distributionId - Distribution ID
80
+ * @returns {boolean} True if deleted
81
+ */
82
+ deleteDistribution(distributionId) {
83
+ const data = this._readFile(this.distributionsFile);
84
+
85
+ const index = data.distributions.findIndex(d => d.id === distributionId);
86
+ if (index === -1) {
87
+ return false;
88
+ }
89
+
90
+ data.distributions.splice(index, 1);
91
+ this._writeFile(this.distributionsFile, data);
92
+ return true;
93
+ }
94
+
95
+ /**
96
+ * Save invalidation
97
+ * @param {object} invalidation - Invalidation object
98
+ */
99
+ saveInvalidation(invalidation) {
100
+ const data = this._readFile(this.invalidationsFile);
101
+
102
+ const index = data.invalidations.findIndex(i => i.id === invalidation.id);
103
+ if (index !== -1) {
104
+ data.invalidations[index] = invalidation;
105
+ } else {
106
+ data.invalidations.push(invalidation);
107
+ }
108
+
109
+ this._writeFile(this.invalidationsFile, data);
110
+ }
111
+
112
+ /**
113
+ * Get invalidation
114
+ * @param {string} invalidationId - Invalidation ID
115
+ * @returns {object|null} Invalidation or null
116
+ */
117
+ getInvalidation(invalidationId) {
118
+ const data = this._readFile(this.invalidationsFile);
119
+ return data.invalidations.find(i => i.id === invalidationId) || null;
120
+ }
121
+
122
+ /**
123
+ * Get invalidations by distribution
124
+ * @param {string} distributionId - Distribution ID
125
+ * @returns {object[]} Array of invalidations
126
+ */
127
+ getInvalidationsByDistribution(distributionId) {
128
+ const data = this._readFile(this.invalidationsFile);
129
+ return data.invalidations.filter(i => i.distributionId === distributionId) || [];
130
+ }
131
+
132
+ /**
133
+ * Delete invalidation
134
+ * @param {string} invalidationId - Invalidation ID
135
+ * @returns {boolean} True if deleted
136
+ */
137
+ deleteInvalidation(invalidationId) {
138
+ const data = this._readFile(this.invalidationsFile);
139
+
140
+ const index = data.invalidations.findIndex(i => i.id === invalidationId);
141
+ if (index === -1) {
142
+ return false;
143
+ }
144
+
145
+ data.invalidations.splice(index, 1);
146
+ this._writeFile(this.invalidationsFile, data);
147
+ return true;
148
+ }
149
+
150
+ /**
151
+ * Save key pair
152
+ * @param {object} keyPair - Key pair object
153
+ */
154
+ saveKeyPair(keyPair) {
155
+ const data = this._readFile(this.keyPairsFile);
156
+
157
+ const index = data.keyPairs.findIndex(k => k.id === keyPair.id);
158
+ if (index !== -1) {
159
+ data.keyPairs[index] = keyPair;
160
+ } else {
161
+ data.keyPairs.push(keyPair);
162
+ }
163
+
164
+ this._writeFile(this.keyPairsFile, data);
165
+ }
166
+
167
+ /**
168
+ * Get key pair
169
+ * @param {string} keyPairId - Key pair ID
170
+ * @returns {object|null} Key pair or null
171
+ */
172
+ getKeyPair(keyPairId) {
173
+ const data = this._readFile(this.keyPairsFile);
174
+ return data.keyPairs.find(k => k.id === keyPairId) || null;
175
+ }
176
+
177
+ /**
178
+ * Get all key pairs
179
+ * @returns {object[]} Array of key pairs
180
+ */
181
+ getAllKeyPairs() {
182
+ const data = this._readFile(this.keyPairsFile);
183
+ return data.keyPairs || [];
184
+ }
185
+
186
+ /**
187
+ * Delete key pair
188
+ * @param {string} keyPairId - Key pair ID
189
+ * @returns {boolean} True if deleted
190
+ */
191
+ deleteKeyPair(keyPairId) {
192
+ const data = this._readFile(this.keyPairsFile);
193
+
194
+ const index = data.keyPairs.findIndex(k => k.id === keyPairId);
195
+ if (index === -1) {
196
+ return false;
197
+ }
198
+
199
+ data.keyPairs.splice(index, 1);
200
+ this._writeFile(this.keyPairsFile, data);
201
+ return true;
202
+ }
203
+
204
+ /**
205
+ * Export all data
206
+ * @returns {object} All data in the store
207
+ */
208
+ export() {
209
+ return {
210
+ distributions: this.getAllDistributions(),
211
+ invalidations: this._readFile(this.invalidationsFile).invalidations || [],
212
+ keyPairs: this.getAllKeyPairs(),
213
+ exportedAt: new Date().toISOString()
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Import data
219
+ * @param {object} data - Data to import
220
+ */
221
+ import(data) {
222
+ if (data.distributions && Array.isArray(data.distributions)) {
223
+ const distributionData = { distributions: data.distributions };
224
+ this._writeFile(this.distributionsFile, distributionData);
225
+ }
226
+
227
+ if (data.invalidations && Array.isArray(data.invalidations)) {
228
+ const invalidationData = { invalidations: data.invalidations };
229
+ this._writeFile(this.invalidationsFile, invalidationData);
230
+ }
231
+
232
+ if (data.keyPairs && Array.isArray(data.keyPairs)) {
233
+ const keyPairData = { keyPairs: data.keyPairs };
234
+ this._writeFile(this.keyPairsFile, keyPairData);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Clear all data
240
+ */
241
+ clear() {
242
+ this._writeFile(this.distributionsFile, { distributions: [] });
243
+ this._writeFile(this.invalidationsFile, { invalidations: [] });
244
+ this._writeFile(this.keyPairsFile, { keyPairs: [] });
245
+ }
246
+
247
+ /**
248
+ * Get storage stats
249
+ * @returns {object} Storage statistics
250
+ */
251
+ getStats() {
252
+ const distributionData = this._readFile(this.distributionsFile);
253
+ const invalidationData = this._readFile(this.invalidationsFile);
254
+ const keyPairData = this._readFile(this.keyPairsFile);
255
+
256
+ return {
257
+ distributions: distributionData.distributions?.length || 0,
258
+ invalidations: invalidationData.invalidations?.length || 0,
259
+ keyPairs: keyPairData.keyPairs?.length || 0,
260
+ storePath: this.storePath,
261
+ lastModified: new Date().toISOString()
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Create a backup
267
+ * @param {string} [backupPath] - Optional custom backup path
268
+ * @returns {string} Path to backup file
269
+ */
270
+ createBackup(backupPath) {
271
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
272
+ const backup = backupPath || path.join(this.storePath, `backup-${timestamp}.json`);
273
+
274
+ const data = this.export();
275
+ this._writeFile(backup, data);
276
+
277
+ return backup;
278
+ }
279
+
280
+ /**
281
+ * Restore from backup
282
+ * @param {string} backupPath - Path to backup file
283
+ */
284
+ restoreFromBackup(backupPath) {
285
+ if (!fs.existsSync(backupPath)) {
286
+ throw new Error(`Backup file not found: ${backupPath}`);
287
+ }
288
+
289
+ const data = this._readFile(backupPath);
290
+ this.import(data);
291
+ }
292
+
293
+ /**
294
+ * Private Methods
295
+ */
296
+
297
+ _readFile(filePath) {
298
+ try {
299
+ if (!fs.existsSync(filePath)) {
300
+ return {};
301
+ }
302
+
303
+ const content = fs.readFileSync(filePath, 'utf-8');
304
+ return JSON.parse(content);
305
+ } catch (error) {
306
+ console.error(`Error reading file ${filePath}:`, error);
307
+ return {};
308
+ }
309
+ }
310
+
311
+ _writeFile(filePath, data) {
312
+ try {
313
+ const directory = path.dirname(filePath);
314
+ if (!fs.existsSync(directory)) {
315
+ fs.mkdirSync(directory, { recursive: true });
316
+ }
317
+
318
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
319
+ } catch (error) {
320
+ console.error(`Error writing file ${filePath}:`, error);
321
+ throw error;
322
+ }
323
+ }
324
+ }
325
+
326
+ module.exports = CloudFrontStore;
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Builds the environment variables for a CloudShell PTY session.
5
+ * Includes AWS credentials and endpoint URLs for all active services.
6
+ *
7
+ * @param {Function} getActiveServices - returns array of { name, port }
8
+ * @returns {Object} environment variables object
9
+ */
10
+ function buildShellEnvironment(getActiveServices) {
11
+ const env = {
12
+ ...process.env,
13
+ AWS_ACCESS_KEY_ID: 'local-access-key',
14
+ AWS_SECRET_ACCESS_KEY: 'local-secret-key',
15
+ AWS_DEFAULT_REGION: 'us-east-1',
16
+ };
17
+
18
+ const services = getActiveServices();
19
+ for (const { name, port } of services) {
20
+ const envKey = `AWS_ENDPOINT_${name.toUpperCase().replace(/-/g, '_')}`;
21
+ env[envKey] = `http://localhost:${port}`;
22
+ }
23
+
24
+ return env;
25
+ }
26
+
27
+ module.exports = { buildShellEnvironment };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const express = require('express');
5
+
6
+ /**
7
+ * Initializes CloudShell: registers routes, sets up WebSocket server,
8
+ * and hooks into the server lifecycle.
9
+ *
10
+ * If node-pty is not available (e.g., native compilation failed),
11
+ * logs a warning and returns a no-op shutdown handle.
12
+ *
13
+ * @param {import('express').Application} app - Management API Express app
14
+ * @param {import('http').Server} httpServer - the underlying HTTP server
15
+ * @param {Function} getActiveServices - function to query active services
16
+ * @returns {{ shutdown: Function }} cleanup handle
17
+ */
18
+ function initializeCloudShell(app, httpServer, getActiveServices) {
19
+ try {
20
+ // Attempt to load node-pty to verify it's available
21
+ require('node-pty');
22
+ } catch (err) {
23
+ console.warn('CloudShell: node-pty not available, feature disabled');
24
+ return { shutdown: () => {} };
25
+ }
26
+
27
+ const { SessionManager } = require('./session-manager');
28
+ const { createWebSocketServer } = require('./websocket-handler');
29
+ const { registerRoutes } = require('./routes');
30
+
31
+ // Instantiate session manager
32
+ const sessionManager = new SessionManager();
33
+
34
+ // Register REST API routes
35
+ registerRoutes(app, sessionManager, getActiveServices);
36
+
37
+ // Set up WebSocket server
38
+ createWebSocketServer(httpServer, sessionManager);
39
+
40
+ // Serve static files (frontend) at /cloudshell
41
+ app.use('/cloudshell', express.static(path.join(__dirname, 'static')));
42
+
43
+ return {
44
+ shutdown: () => sessionManager.destroyAll(),
45
+ };
46
+ }
47
+
48
+ module.exports = { initializeCloudShell };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const { buildShellEnvironment } = require('./env-builder');
4
+
5
+ /**
6
+ * Registers CloudShell REST API routes on the Express app.
7
+ *
8
+ * @param {import('express').Application} app - the Management API Express app
9
+ * @param {import('./session-manager').SessionManager} sessionManager - session manager instance
10
+ * @param {Function} getActiveServices - function returning active services and ports
11
+ */
12
+ function registerRoutes(app, sessionManager, getActiveServices) {
13
+ /**
14
+ * GET /__admin/cloudshell/sessions
15
+ * Returns all active sessions with IDs and creation timestamps.
16
+ */
17
+ app.get('/__admin/cloudshell/sessions', (req, res) => {
18
+ console.log('CloudShell: GET /sessions received');
19
+ const sessions = sessionManager.listSessions();
20
+ res.json({ sessions });
21
+ });
22
+
23
+ /**
24
+ * POST /__admin/cloudshell/sessions
25
+ * Creates a new CloudShell session with pre-configured environment.
26
+ * Returns 201 with { id, createdAt } on success.
27
+ * Returns 500 with { error } if PTY spawn fails.
28
+ */
29
+ app.post('/__admin/cloudshell/sessions', (req, res) => {
30
+ console.log('CloudShell: POST /sessions received');
31
+ try {
32
+ const env = buildShellEnvironment(getActiveServices);
33
+ console.log('CloudShell: Environment built, creating session...');
34
+ const session = sessionManager.createSession(env);
35
+ console.log('CloudShell: Session created:', session.id);
36
+ res.status(201).json({ id: session.id, createdAt: session.createdAt });
37
+ } catch (err) {
38
+ console.error('CloudShell: Failed to create session:', err.message);
39
+ res.status(500).json({ error: 'Failed to create session: ' + err.message });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * DELETE /__admin/cloudshell/sessions/:id
45
+ * Terminates a specific session.
46
+ * Returns 200 on success, 404 if session not found.
47
+ */
48
+ app.delete('/__admin/cloudshell/sessions/:id', (req, res) => {
49
+ const { id } = req.params;
50
+ const destroyed = sessionManager.destroySession(id);
51
+
52
+ if (destroyed) {
53
+ res.status(200).json({});
54
+ } else {
55
+ res.status(404).json({ error: 'Session not found' });
56
+ }
57
+ });
58
+ }
59
+
60
+ module.exports = { registerRoutes };
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const pty = require('node-pty');
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+ /**
7
+ * Returns the default shell for the current platform.
8
+ * @returns {string} shell executable path or name
9
+ */
10
+ function getDefaultShell() {
11
+ if (process.platform === 'win32') {
12
+ return 'powershell.exe';
13
+ }
14
+ return process.env.SHELL || '/bin/bash';
15
+ }
16
+
17
+ /**
18
+ * @typedef {Object} Session
19
+ * @property {string} id - UUID v4 session identifier
20
+ * @property {import('node-pty').IPty} pty - node-pty process instance
21
+ * @property {import('ws').WebSocket|null} ws - currently attached WebSocket (null if disconnected)
22
+ * @property {string} createdAt - ISO timestamp of session creation
23
+ */
24
+
25
+ /**
26
+ * Manages the lifecycle of CloudShell terminal sessions.
27
+ * Stores sessions in an in-memory Map<string, Session>.
28
+ */
29
+ class SessionManager {
30
+ constructor() {
31
+ /** @type {Map<string, Session>} */
32
+ this.sessions = new Map();
33
+ }
34
+
35
+ /**
36
+ * Creates a new PTY session with pre-configured environment.
37
+ * @param {Object} env - environment variables to inject into the PTY process
38
+ * @returns {Session} the newly created session
39
+ */
40
+ createSession(env) {
41
+ const shell = getDefaultShell();
42
+ const id = uuidv4();
43
+
44
+ let ptyProcess;
45
+ try {
46
+ ptyProcess = pty.spawn(shell, [], {
47
+ name: 'xterm-256color',
48
+ cols: 80,
49
+ rows: 24,
50
+ cwd: process.cwd(),
51
+ env: env,
52
+ });
53
+ } catch (err) {
54
+ throw new Error(`Failed to spawn PTY (${shell}): ${err.message}`);
55
+ }
56
+
57
+ const session = {
58
+ id,
59
+ pty: ptyProcess,
60
+ ws: null,
61
+ createdAt: new Date().toISOString(),
62
+ };
63
+
64
+ // Handle unexpected PTY exit to clean up session
65
+ ptyProcess.onExit(({ exitCode, signal }) => {
66
+ // Only clean up if session still exists (wasn't explicitly destroyed)
67
+ if (this.sessions.has(id)) {
68
+ const s = this.sessions.get(id);
69
+ if (s && s.ws) {
70
+ try {
71
+ s.ws.close(1011, 'PTY process exited');
72
+ } catch (e) { /* ignore */ }
73
+ }
74
+ this.sessions.delete(id);
75
+ }
76
+ });
77
+
78
+ this.sessions.set(id, session);
79
+ return session;
80
+ }
81
+
82
+ /**
83
+ * Returns a session by ID, or undefined if not found.
84
+ * @param {string} id - session ID
85
+ * @returns {Session|undefined}
86
+ */
87
+ getSession(id) {
88
+ return this.sessions.get(id);
89
+ }
90
+
91
+ /**
92
+ * Lists all active sessions.
93
+ * @returns {Array<{id: string, createdAt: string}>}
94
+ */
95
+ listSessions() {
96
+ const result = [];
97
+ for (const session of this.sessions.values()) {
98
+ result.push({ id: session.id, createdAt: session.createdAt });
99
+ }
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Terminates a session: kills PTY, closes WebSocket, removes from map.
105
+ * @param {string} id - session ID to destroy
106
+ * @returns {boolean} true if session existed and was terminated
107
+ */
108
+ destroySession(id) {
109
+ const session = this.sessions.get(id);
110
+ if (!session) {
111
+ return false;
112
+ }
113
+
114
+ try {
115
+ session.pty.kill();
116
+ } catch (err) {
117
+ // PTY may already be dead; ignore
118
+ }
119
+
120
+ if (session.ws) {
121
+ try {
122
+ session.ws.close(1011, 'Session terminated');
123
+ } catch (err) {
124
+ // WebSocket may already be closed; ignore
125
+ }
126
+ session.ws = null;
127
+ }
128
+
129
+ this.sessions.delete(id);
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Terminates all sessions. Called on server shutdown.
135
+ */
136
+ destroyAll() {
137
+ for (const id of this.sessions.keys()) {
138
+ this.destroySession(id);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Attaches a WebSocket to an existing session and pipes PTY output to WS.
144
+ * @param {string} id - session ID
145
+ * @param {import('ws').WebSocket} ws - WebSocket connection to attach
146
+ * @returns {boolean} true if session exists and was attached
147
+ */
148
+ attachWebSocket(id, ws) {
149
+ const session = this.sessions.get(id);
150
+ if (!session) {
151
+ return false;
152
+ }
153
+
154
+ // Detach any previously attached WebSocket
155
+ if (session.ws) {
156
+ try {
157
+ session.ws.close(1000, 'Replaced by new connection');
158
+ } catch (err) {
159
+ // ignore
160
+ }
161
+ }
162
+
163
+ session.ws = ws;
164
+
165
+ // Pipe PTY output to WebSocket
166
+ const dataListener = session.pty.onData((data) => {
167
+ if (session.ws && session.ws.readyState === 1) { // WebSocket.OPEN === 1
168
+ session.ws.send(data);
169
+ }
170
+ });
171
+
172
+ // Clean up listener when WebSocket closes
173
+ ws.on('close', () => {
174
+ dataListener.dispose();
175
+ if (session.ws === ws) {
176
+ session.ws = null;
177
+ }
178
+ });
179
+
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Detaches the WebSocket from a session (keeps PTY alive).
185
+ * @param {string} id - session ID
186
+ */
187
+ detachWebSocket(id) {
188
+ const session = this.sessions.get(id);
189
+ if (!session || !session.ws) {
190
+ return;
191
+ }
192
+
193
+ try {
194
+ session.ws.close(1000, 'Detached');
195
+ } catch (err) {
196
+ // ignore
197
+ }
198
+ session.ws = null;
199
+ }
200
+
201
+ /**
202
+ * Resizes the PTY for a given session. Ignores invalid (≤0) values.
203
+ * @param {string} id - session ID
204
+ * @param {number} cols - new column count
205
+ * @param {number} rows - new row count
206
+ */
207
+ resizeSession(id, cols, rows) {
208
+ const session = this.sessions.get(id);
209
+ if (!session) {
210
+ return;
211
+ }
212
+
213
+ // Ignore invalid dimensions
214
+ if (!cols || !rows || cols <= 0 || rows <= 0) {
215
+ return;
216
+ }
217
+
218
+ try {
219
+ session.pty.resize(cols, rows);
220
+ } catch (err) {
221
+ // PTY may have exited; ignore resize errors
222
+ }
223
+ }
224
+ }
225
+
226
+ module.exports = { SessionManager, getDefaultShell };
File without changes