@de-otio/chaoskb-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/lib/admin-handler/index.d.ts +22 -0
  2. package/dist/lib/admin-handler/index.d.ts.map +1 -0
  3. package/dist/lib/admin-handler/index.js +92 -0
  4. package/dist/lib/admin-handler/index.js.map +1 -0
  5. package/dist/lib/admin-handler/index.ts +123 -0
  6. package/dist/lib/admin-handler/routes/metrics.d.ts +11 -0
  7. package/dist/lib/admin-handler/routes/metrics.d.ts.map +1 -0
  8. package/dist/lib/admin-handler/routes/metrics.js +200 -0
  9. package/dist/lib/admin-handler/routes/metrics.js.map +1 -0
  10. package/dist/lib/admin-handler/routes/metrics.ts +234 -0
  11. package/dist/lib/admin-handler/routes/overview.d.ts +9 -0
  12. package/dist/lib/admin-handler/routes/overview.d.ts.map +1 -0
  13. package/dist/lib/admin-handler/routes/overview.js +110 -0
  14. package/dist/lib/admin-handler/routes/overview.js.map +1 -0
  15. package/dist/lib/admin-handler/routes/overview.ts +133 -0
  16. package/dist/lib/admin-handler/routes/tenants.d.ts +10 -0
  17. package/dist/lib/admin-handler/routes/tenants.d.ts.map +1 -0
  18. package/dist/lib/admin-handler/routes/tenants.js +108 -0
  19. package/dist/lib/admin-handler/routes/tenants.js.map +1 -0
  20. package/dist/lib/admin-handler/routes/tenants.ts +134 -0
  21. package/dist/lib/chaoskb-stack.d.ts +22 -0
  22. package/dist/lib/chaoskb-stack.d.ts.map +1 -0
  23. package/dist/lib/chaoskb-stack.js +60 -0
  24. package/dist/lib/chaoskb-stack.js.map +1 -0
  25. package/dist/lib/constructs/admin-api.d.ts +16 -0
  26. package/dist/lib/constructs/admin-api.d.ts.map +1 -0
  27. package/dist/lib/constructs/admin-api.js +93 -0
  28. package/dist/lib/constructs/admin-api.js.map +1 -0
  29. package/dist/lib/constructs/admin-dashboard.d.ts +18 -0
  30. package/dist/lib/constructs/admin-dashboard.d.ts.map +1 -0
  31. package/dist/lib/constructs/admin-dashboard.js +172 -0
  32. package/dist/lib/constructs/admin-dashboard.js.map +1 -0
  33. package/dist/lib/constructs/api.d.ts +17 -0
  34. package/dist/lib/constructs/api.d.ts.map +1 -0
  35. package/dist/lib/constructs/api.js +81 -0
  36. package/dist/lib/constructs/api.js.map +1 -0
  37. package/dist/lib/constructs/auth.d.ts +11 -0
  38. package/dist/lib/constructs/auth.d.ts.map +1 -0
  39. package/dist/lib/constructs/auth.js +18 -0
  40. package/dist/lib/constructs/auth.js.map +1 -0
  41. package/dist/lib/constructs/blob-store.d.ts +10 -0
  42. package/dist/lib/constructs/blob-store.d.ts.map +1 -0
  43. package/dist/lib/constructs/blob-store.js +31 -0
  44. package/dist/lib/constructs/blob-store.js.map +1 -0
  45. package/dist/lib/deploy-cli.d.ts +3 -0
  46. package/dist/lib/deploy-cli.d.ts.map +1 -0
  47. package/dist/lib/deploy-cli.js +49 -0
  48. package/dist/lib/deploy-cli.js.map +1 -0
  49. package/dist/lib/handler/index.d.ts +23 -0
  50. package/dist/lib/handler/index.d.ts.map +1 -0
  51. package/dist/lib/handler/index.js +276 -0
  52. package/dist/lib/handler/index.js.map +1 -0
  53. package/dist/lib/handler/index.ts +372 -0
  54. package/dist/lib/handler/logger.d.ts +16 -0
  55. package/dist/lib/handler/logger.d.ts.map +1 -0
  56. package/dist/lib/handler/logger.js +26 -0
  57. package/dist/lib/handler/logger.js.map +1 -0
  58. package/dist/lib/handler/logger.ts +36 -0
  59. package/dist/lib/handler/middleware/input-validation.d.ts +6 -0
  60. package/dist/lib/handler/middleware/input-validation.d.ts.map +1 -0
  61. package/dist/lib/handler/middleware/input-validation.js +36 -0
  62. package/dist/lib/handler/middleware/input-validation.js.map +1 -0
  63. package/dist/lib/handler/middleware/input-validation.ts +44 -0
  64. package/dist/lib/handler/middleware/rate-limit.d.ts +14 -0
  65. package/dist/lib/handler/middleware/rate-limit.d.ts.map +1 -0
  66. package/dist/lib/handler/middleware/rate-limit.js +94 -0
  67. package/dist/lib/handler/middleware/rate-limit.js.map +1 -0
  68. package/dist/lib/handler/middleware/rate-limit.ts +121 -0
  69. package/dist/lib/handler/middleware/ssh-auth.d.ts +48 -0
  70. package/dist/lib/handler/middleware/ssh-auth.d.ts.map +1 -0
  71. package/dist/lib/handler/middleware/ssh-auth.js +256 -0
  72. package/dist/lib/handler/middleware/ssh-auth.js.map +1 -0
  73. package/dist/lib/handler/middleware/ssh-auth.ts +300 -0
  74. package/dist/lib/handler/routes/audit.d.ts +24 -0
  75. package/dist/lib/handler/routes/audit.d.ts.map +1 -0
  76. package/dist/lib/handler/routes/audit.js +94 -0
  77. package/dist/lib/handler/routes/audit.js.map +1 -0
  78. package/dist/lib/handler/routes/audit.ts +101 -0
  79. package/dist/lib/handler/routes/blobs.d.ts +13 -0
  80. package/dist/lib/handler/routes/blobs.d.ts.map +1 -0
  81. package/dist/lib/handler/routes/blobs.js +298 -0
  82. package/dist/lib/handler/routes/blobs.js.map +1 -0
  83. package/dist/lib/handler/routes/blobs.ts +348 -0
  84. package/dist/lib/handler/routes/devices.d.ts +48 -0
  85. package/dist/lib/handler/routes/devices.d.ts.map +1 -0
  86. package/dist/lib/handler/routes/devices.js +394 -0
  87. package/dist/lib/handler/routes/devices.js.map +1 -0
  88. package/dist/lib/handler/routes/devices.ts +458 -0
  89. package/dist/lib/handler/routes/export.d.ts +9 -0
  90. package/dist/lib/handler/routes/export.d.ts.map +1 -0
  91. package/dist/lib/handler/routes/export.js +40 -0
  92. package/dist/lib/handler/routes/export.js.map +1 -0
  93. package/dist/lib/handler/routes/export.ts +55 -0
  94. package/dist/lib/handler/routes/github.d.ts +31 -0
  95. package/dist/lib/handler/routes/github.d.ts.map +1 -0
  96. package/dist/lib/handler/routes/github.js +118 -0
  97. package/dist/lib/handler/routes/github.js.map +1 -0
  98. package/dist/lib/handler/routes/github.ts +162 -0
  99. package/dist/lib/handler/routes/health.d.ts +6 -0
  100. package/dist/lib/handler/routes/health.d.ts.map +1 -0
  101. package/dist/lib/handler/routes/health.js +14 -0
  102. package/dist/lib/handler/routes/health.js.map +1 -0
  103. package/dist/lib/handler/routes/health.ts +10 -0
  104. package/dist/lib/handler/routes/invites.d.ts +24 -0
  105. package/dist/lib/handler/routes/invites.d.ts.map +1 -0
  106. package/dist/lib/handler/routes/invites.js +445 -0
  107. package/dist/lib/handler/routes/invites.js.map +1 -0
  108. package/dist/lib/handler/routes/invites.ts +527 -0
  109. package/dist/lib/handler/routes/notifications.d.ts +39 -0
  110. package/dist/lib/handler/routes/notifications.d.ts.map +1 -0
  111. package/dist/lib/handler/routes/notifications.js +150 -0
  112. package/dist/lib/handler/routes/notifications.js.map +1 -0
  113. package/dist/lib/handler/routes/notifications.ts +163 -0
  114. package/dist/lib/handler/routes/projects.d.ts +24 -0
  115. package/dist/lib/handler/routes/projects.d.ts.map +1 -0
  116. package/dist/lib/handler/routes/projects.js +47 -0
  117. package/dist/lib/handler/routes/projects.js.map +1 -0
  118. package/dist/lib/handler/routes/projects.ts +69 -0
  119. package/dist/lib/handler/routes/register.d.ts +19 -0
  120. package/dist/lib/handler/routes/register.d.ts.map +1 -0
  121. package/dist/lib/handler/routes/register.js +327 -0
  122. package/dist/lib/handler/routes/register.js.map +1 -0
  123. package/dist/lib/handler/routes/register.ts +363 -0
  124. package/dist/lib/handler/routes/restore.d.ts +9 -0
  125. package/dist/lib/handler/routes/restore.d.ts.map +1 -0
  126. package/dist/lib/handler/routes/restore.js +52 -0
  127. package/dist/lib/handler/routes/restore.js.map +1 -0
  128. package/dist/lib/handler/routes/restore.ts +73 -0
  129. package/dist/lib/handler/routes/revocation.d.ts +13 -0
  130. package/dist/lib/handler/routes/revocation.d.ts.map +1 -0
  131. package/dist/lib/handler/routes/revocation.js +63 -0
  132. package/dist/lib/handler/routes/revocation.js.map +1 -0
  133. package/dist/lib/handler/routes/revocation.ts +87 -0
  134. package/dist/lib/handler/routes/rotation.d.ts +24 -0
  135. package/dist/lib/handler/routes/rotation.d.ts.map +1 -0
  136. package/dist/lib/handler/routes/rotation.js +291 -0
  137. package/dist/lib/handler/routes/rotation.js.map +1 -0
  138. package/dist/lib/handler/routes/rotation.ts +336 -0
  139. package/dist/lib/handler/routes/tenants.d.ts +11 -0
  140. package/dist/lib/handler/routes/tenants.d.ts.map +1 -0
  141. package/dist/lib/handler/routes/tenants.js +181 -0
  142. package/dist/lib/handler/routes/tenants.js.map +1 -0
  143. package/dist/lib/handler/routes/tenants.ts +198 -0
  144. package/dist/lib/handler/routes/wrapped-key.d.ts +21 -0
  145. package/dist/lib/handler/routes/wrapped-key.d.ts.map +1 -0
  146. package/dist/lib/handler/routes/wrapped-key.js +76 -0
  147. package/dist/lib/handler/routes/wrapped-key.js.map +1 -0
  148. package/dist/lib/handler/routes/wrapped-key.ts +108 -0
  149. package/dist/lib/index.d.ts +7 -0
  150. package/dist/lib/index.d.ts.map +1 -0
  151. package/dist/lib/index.js +16 -0
  152. package/dist/lib/index.js.map +1 -0
  153. package/dist/vitest.config.d.ts +3 -0
  154. package/dist/vitest.config.d.ts.map +1 -0
  155. package/dist/vitest.config.js +18 -0
  156. package/dist/vitest.config.js.map +1 -0
  157. package/package.json +61 -0
@@ -0,0 +1,234 @@
1
+ import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
2
+ import { CloudWatchClient, DescribeAlarmsCommand } from '@aws-sdk/client-cloudwatch';
3
+ import {
4
+ CostExplorerClient,
5
+ GetCostAndUsageCommand,
6
+ GetCostForecastCommand,
7
+ } from '@aws-sdk/client-cost-explorer';
8
+ import { logger } from '../../handler/logger.js';
9
+
10
+ interface RouteResult {
11
+ statusCode: number;
12
+ body: string;
13
+ headers: Record<string, string>;
14
+ }
15
+
16
+ export async function handleUsageMetrics(
17
+ ddb: DynamoDBDocumentClient,
18
+ tableName: string,
19
+ environment: string,
20
+ ): Promise<RouteResult> {
21
+ try {
22
+ // Total tenants: Scan count where SK='META'
23
+ let totalTenants = 0;
24
+ let totalBlobs = 0;
25
+ let totalStorageBytes = 0;
26
+ let active7d = 0;
27
+ let active30d = 0;
28
+
29
+ const now = new Date();
30
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
31
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
32
+
33
+ // Scan META records for tenant stats
34
+ let exclusiveStartKey: Record<string, unknown> | undefined;
35
+ do {
36
+ const result = await ddb.send(
37
+ new ScanCommand({
38
+ TableName: tableName,
39
+ FilterExpression: 'SK = :sk',
40
+ ExpressionAttributeValues: { ':sk': 'META' },
41
+ ProjectionExpression: 'storageUsedBytes, updatedAt',
42
+ ExclusiveStartKey: exclusiveStartKey,
43
+ }),
44
+ );
45
+
46
+ if (result.Items) {
47
+ totalTenants += result.Items.length;
48
+ for (const item of result.Items) {
49
+ totalStorageBytes += (item['storageUsedBytes'] as number) ?? 0;
50
+ const updatedAt = item['updatedAt'] as string | undefined;
51
+ if (updatedAt && updatedAt >= sevenDaysAgo) active7d++;
52
+ if (updatedAt && updatedAt >= thirtyDaysAgo) active30d++;
53
+ }
54
+ }
55
+ exclusiveStartKey = result.LastEvaluatedKey;
56
+ } while (exclusiveStartKey);
57
+
58
+ // Scan BLOB records for total blob count
59
+ let blobStartKey: Record<string, unknown> | undefined;
60
+ do {
61
+ const result = await ddb.send(
62
+ new ScanCommand({
63
+ TableName: tableName,
64
+ FilterExpression: 'begins_with(SK, :sk)',
65
+ ExpressionAttributeValues: { ':sk': 'BLOB#' },
66
+ Select: 'COUNT',
67
+ ExclusiveStartKey: blobStartKey,
68
+ }),
69
+ );
70
+
71
+ totalBlobs += result.Count ?? 0;
72
+ blobStartKey = result.LastEvaluatedKey;
73
+ } while (blobStartKey);
74
+
75
+ // Mock chart data (would come from CloudWatch in production)
76
+ const chartData = {
77
+ dailySyncRequests: Array.from({ length: 7 }, (_, i) => ({
78
+ date: new Date(now.getTime() - (6 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
79
+ count: Math.floor(Math.random() * 100) + 10,
80
+ })),
81
+ tenantGrowth: Array.from({ length: 30 }, (_, i) => ({
82
+ date: new Date(now.getTime() - (29 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
83
+ total: totalTenants - (29 - i),
84
+ })),
85
+ };
86
+
87
+ return {
88
+ statusCode: 200,
89
+ body: JSON.stringify({
90
+ environment,
91
+ totalTenants,
92
+ totalBlobs,
93
+ totalStorageBytes,
94
+ active7d,
95
+ active30d,
96
+ charts: chartData,
97
+ }),
98
+ headers: { 'Content-Type': 'application/json' },
99
+ };
100
+ } catch (err) {
101
+ logger.error('Failed to get usage metrics', { error: String(err) });
102
+ return {
103
+ statusCode: 500,
104
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get usage metrics' }),
105
+ headers: { 'Content-Type': 'application/json' },
106
+ };
107
+ }
108
+ }
109
+
110
+ export async function handleHealthMetrics(environment: string): Promise<RouteResult> {
111
+ try {
112
+ const cw = new CloudWatchClient({});
113
+ const alarmsResult = await cw.send(
114
+ new DescribeAlarmsCommand({
115
+ AlarmNamePrefix: `chaoskb-${environment}-`,
116
+ }),
117
+ );
118
+
119
+ const alarms = alarmsResult.MetricAlarms ?? [];
120
+ const services = alarms.map((alarm) => ({
121
+ name: alarm.AlarmName ?? 'unknown',
122
+ status: alarm.StateValue === 'OK' ? 'healthy' : alarm.StateValue === 'ALARM' ? 'unhealthy' : 'unknown',
123
+ message: alarm.StateReason ?? '',
124
+ }));
125
+
126
+ // If no alarms are configured, report the basic service as healthy
127
+ if (services.length === 0) {
128
+ services.push(
129
+ { name: 'api', status: 'healthy', message: 'No alarms configured' },
130
+ { name: 'database', status: 'healthy', message: 'No alarms configured' },
131
+ );
132
+ }
133
+
134
+ return {
135
+ statusCode: 200,
136
+ body: JSON.stringify({
137
+ environment,
138
+ services,
139
+ incidents: [],
140
+ }),
141
+ headers: { 'Content-Type': 'application/json' },
142
+ };
143
+ } catch (err) {
144
+ logger.error('Failed to get health metrics', { error: String(err) });
145
+ return {
146
+ statusCode: 500,
147
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get health metrics' }),
148
+ headers: { 'Content-Type': 'application/json' },
149
+ };
150
+ }
151
+ }
152
+
153
+ export async function handleCostMetrics(environment: string): Promise<RouteResult> {
154
+ try {
155
+ const ce = new CostExplorerClient({ region: 'us-east-1' });
156
+
157
+ const now = new Date();
158
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
159
+ const today = now.toISOString().split('T')[0];
160
+ const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().split('T')[0];
161
+
162
+ // MTD spend by service
163
+ const costResult = await ce.send(
164
+ new GetCostAndUsageCommand({
165
+ TimePeriod: { Start: startOfMonth, End: today },
166
+ Granularity: 'DAILY',
167
+ Metrics: ['UnblendedCost'],
168
+ GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],
169
+ Filter: {
170
+ Tags: {
171
+ Key: 'Project',
172
+ Values: [`chaoskb-${environment}`],
173
+ },
174
+ },
175
+ }),
176
+ );
177
+
178
+ const monthlySpend = (costResult.ResultsByTime ?? []).map((period) => ({
179
+ date: period.TimePeriod?.Start ?? '',
180
+ total: (period.Groups ?? []).reduce((sum, g) => {
181
+ return sum + parseFloat(g.Metrics?.['UnblendedCost']?.Amount ?? '0');
182
+ }, 0),
183
+ }));
184
+
185
+ const spendByService: Record<string, number> = {};
186
+ for (const period of costResult.ResultsByTime ?? []) {
187
+ for (const group of period.Groups ?? []) {
188
+ const service = group.Keys?.[0] ?? 'Unknown';
189
+ const amount = parseFloat(group.Metrics?.['UnblendedCost']?.Amount ?? '0');
190
+ spendByService[service] = (spendByService[service] ?? 0) + amount;
191
+ }
192
+ }
193
+
194
+ // Forecast for rest of month
195
+ let forecast: { forecastedTotal: string; currency: string } | null = null;
196
+ try {
197
+ const forecastResult = await ce.send(
198
+ new GetCostForecastCommand({
199
+ TimePeriod: { Start: today, End: endOfMonth },
200
+ Metric: 'UNBLENDED_COST',
201
+ Granularity: 'MONTHLY',
202
+ }),
203
+ );
204
+ forecast = {
205
+ forecastedTotal: forecastResult.Total?.Amount ?? '0',
206
+ currency: forecastResult.Total?.Unit ?? 'USD',
207
+ };
208
+ } catch {
209
+ // Forecast may fail if not enough historical data
210
+ logger.warn('Cost forecast unavailable');
211
+ }
212
+
213
+ return {
214
+ statusCode: 200,
215
+ body: JSON.stringify({
216
+ environment,
217
+ monthlySpend,
218
+ spendByService: Object.entries(spendByService).map(([service, amount]) => ({
219
+ service,
220
+ amount: Math.round(amount * 100) / 100,
221
+ })),
222
+ forecast,
223
+ }),
224
+ headers: { 'Content-Type': 'application/json' },
225
+ };
226
+ } catch (err) {
227
+ logger.error('Failed to get cost metrics', { error: String(err) });
228
+ return {
229
+ statusCode: 500,
230
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get cost metrics' }),
231
+ headers: { 'Content-Type': 'application/json' },
232
+ };
233
+ }
234
+ }
@@ -0,0 +1,9 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ interface RouteResult {
3
+ statusCode: number;
4
+ body: string;
5
+ headers: Record<string, string>;
6
+ }
7
+ export declare function handleOverview(ddb: DynamoDBDocumentClient, tableName: string, environment: string): Promise<RouteResult>;
8
+ export {};
9
+ //# sourceMappingURL=overview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/overview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAe,MAAM,uBAAuB,CAAC;AAQ5E,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAkHtB"}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleOverview = handleOverview;
4
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
+ const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
6
+ const client_cost_explorer_1 = require("@aws-sdk/client-cost-explorer");
7
+ const logger_js_1 = require("../../handler/logger.js");
8
+ async function handleOverview(ddb, tableName, environment) {
9
+ try {
10
+ // Tenant count, blob count, and storage total from DynamoDB
11
+ let totalTenants = 0;
12
+ let totalBlobs = 0;
13
+ let totalStorageBytes = 0;
14
+ // Scan META records
15
+ let exclusiveStartKey;
16
+ do {
17
+ const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
18
+ TableName: tableName,
19
+ FilterExpression: 'SK = :sk',
20
+ ExpressionAttributeValues: { ':sk': 'META' },
21
+ ProjectionExpression: 'storageUsedBytes',
22
+ ExclusiveStartKey: exclusiveStartKey,
23
+ }));
24
+ if (result.Items) {
25
+ totalTenants += result.Items.length;
26
+ for (const item of result.Items) {
27
+ totalStorageBytes += item['storageUsedBytes'] ?? 0;
28
+ }
29
+ }
30
+ exclusiveStartKey = result.LastEvaluatedKey;
31
+ } while (exclusiveStartKey);
32
+ // Scan BLOB records for count
33
+ let blobStartKey;
34
+ do {
35
+ const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
36
+ TableName: tableName,
37
+ FilterExpression: 'begins_with(SK, :sk)',
38
+ ExpressionAttributeValues: { ':sk': 'BLOB#' },
39
+ Select: 'COUNT',
40
+ ExclusiveStartKey: blobStartKey,
41
+ }));
42
+ totalBlobs += result.Count ?? 0;
43
+ blobStartKey = result.LastEvaluatedKey;
44
+ } while (blobStartKey);
45
+ // Health status from CloudWatch
46
+ let healthStatus = 'healthy';
47
+ let alarmCount = 0;
48
+ try {
49
+ const cw = new client_cloudwatch_1.CloudWatchClient({});
50
+ const alarmsResult = await cw.send(new client_cloudwatch_1.DescribeAlarmsCommand({
51
+ AlarmNamePrefix: `chaoskb-${environment}-`,
52
+ StateValue: 'ALARM',
53
+ }));
54
+ alarmCount = alarmsResult.MetricAlarms?.length ?? 0;
55
+ if (alarmCount > 0)
56
+ healthStatus = 'unhealthy';
57
+ }
58
+ catch {
59
+ healthStatus = 'degraded';
60
+ }
61
+ // Cost MTD from Cost Explorer
62
+ let costMtd = 0;
63
+ try {
64
+ const ce = new client_cost_explorer_1.CostExplorerClient({ region: 'us-east-1' });
65
+ const now = new Date();
66
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
67
+ const today = now.toISOString().split('T')[0];
68
+ const costResult = await ce.send(new client_cost_explorer_1.GetCostAndUsageCommand({
69
+ TimePeriod: { Start: startOfMonth, End: today },
70
+ Granularity: 'MONTHLY',
71
+ Metrics: ['UnblendedCost'],
72
+ Filter: {
73
+ Tags: {
74
+ Key: 'Project',
75
+ Values: [`chaoskb-${environment}`],
76
+ },
77
+ },
78
+ }));
79
+ for (const period of costResult.ResultsByTime ?? []) {
80
+ costMtd += parseFloat(period.Total?.['UnblendedCost']?.Amount ?? '0');
81
+ }
82
+ }
83
+ catch {
84
+ // Cost data may be unavailable
85
+ logger_js_1.logger.warn('Cost data unavailable for overview');
86
+ }
87
+ return {
88
+ statusCode: 200,
89
+ body: JSON.stringify({
90
+ environment,
91
+ totalTenants,
92
+ totalBlobs,
93
+ totalStorageBytes,
94
+ healthStatus,
95
+ activeAlarms: alarmCount,
96
+ costMtd: Math.round(costMtd * 100) / 100,
97
+ }),
98
+ headers: { 'Content-Type': 'application/json' },
99
+ };
100
+ }
101
+ catch (err) {
102
+ logger_js_1.logger.error('Failed to get overview', { error: String(err) });
103
+ return {
104
+ statusCode: 500,
105
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get overview' }),
106
+ headers: { 'Content-Type': 'application/json' },
107
+ };
108
+ }
109
+ }
110
+ //# sourceMappingURL=overview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overview.js","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/overview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
2
+ import { CloudWatchClient, DescribeAlarmsCommand } from '@aws-sdk/client-cloudwatch';
3
+ import {
4
+ CostExplorerClient,
5
+ GetCostAndUsageCommand,
6
+ } from '@aws-sdk/client-cost-explorer';
7
+ import { logger } from '../../handler/logger.js';
8
+
9
+ interface RouteResult {
10
+ statusCode: number;
11
+ body: string;
12
+ headers: Record<string, string>;
13
+ }
14
+
15
+ export async function handleOverview(
16
+ ddb: DynamoDBDocumentClient,
17
+ tableName: string,
18
+ environment: string,
19
+ ): Promise<RouteResult> {
20
+ try {
21
+ // Tenant count, blob count, and storage total from DynamoDB
22
+ let totalTenants = 0;
23
+ let totalBlobs = 0;
24
+ let totalStorageBytes = 0;
25
+
26
+ // Scan META records
27
+ let exclusiveStartKey: Record<string, unknown> | undefined;
28
+ do {
29
+ const result = await ddb.send(
30
+ new ScanCommand({
31
+ TableName: tableName,
32
+ FilterExpression: 'SK = :sk',
33
+ ExpressionAttributeValues: { ':sk': 'META' },
34
+ ProjectionExpression: 'storageUsedBytes',
35
+ ExclusiveStartKey: exclusiveStartKey,
36
+ }),
37
+ );
38
+
39
+ if (result.Items) {
40
+ totalTenants += result.Items.length;
41
+ for (const item of result.Items) {
42
+ totalStorageBytes += (item['storageUsedBytes'] as number) ?? 0;
43
+ }
44
+ }
45
+ exclusiveStartKey = result.LastEvaluatedKey;
46
+ } while (exclusiveStartKey);
47
+
48
+ // Scan BLOB records for count
49
+ let blobStartKey: Record<string, unknown> | undefined;
50
+ do {
51
+ const result = await ddb.send(
52
+ new ScanCommand({
53
+ TableName: tableName,
54
+ FilterExpression: 'begins_with(SK, :sk)',
55
+ ExpressionAttributeValues: { ':sk': 'BLOB#' },
56
+ Select: 'COUNT',
57
+ ExclusiveStartKey: blobStartKey,
58
+ }),
59
+ );
60
+
61
+ totalBlobs += result.Count ?? 0;
62
+ blobStartKey = result.LastEvaluatedKey;
63
+ } while (blobStartKey);
64
+
65
+ // Health status from CloudWatch
66
+ let healthStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
67
+ let alarmCount = 0;
68
+ try {
69
+ const cw = new CloudWatchClient({});
70
+ const alarmsResult = await cw.send(
71
+ new DescribeAlarmsCommand({
72
+ AlarmNamePrefix: `chaoskb-${environment}-`,
73
+ StateValue: 'ALARM',
74
+ }),
75
+ );
76
+ alarmCount = alarmsResult.MetricAlarms?.length ?? 0;
77
+ if (alarmCount > 0) healthStatus = 'unhealthy';
78
+ } catch {
79
+ healthStatus = 'degraded';
80
+ }
81
+
82
+ // Cost MTD from Cost Explorer
83
+ let costMtd = 0;
84
+ try {
85
+ const ce = new CostExplorerClient({ region: 'us-east-1' });
86
+ const now = new Date();
87
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
88
+ const today = now.toISOString().split('T')[0];
89
+
90
+ const costResult = await ce.send(
91
+ new GetCostAndUsageCommand({
92
+ TimePeriod: { Start: startOfMonth, End: today },
93
+ Granularity: 'MONTHLY',
94
+ Metrics: ['UnblendedCost'],
95
+ Filter: {
96
+ Tags: {
97
+ Key: 'Project',
98
+ Values: [`chaoskb-${environment}`],
99
+ },
100
+ },
101
+ }),
102
+ );
103
+
104
+ for (const period of costResult.ResultsByTime ?? []) {
105
+ costMtd += parseFloat(period.Total?.['UnblendedCost']?.Amount ?? '0');
106
+ }
107
+ } catch {
108
+ // Cost data may be unavailable
109
+ logger.warn('Cost data unavailable for overview');
110
+ }
111
+
112
+ return {
113
+ statusCode: 200,
114
+ body: JSON.stringify({
115
+ environment,
116
+ totalTenants,
117
+ totalBlobs,
118
+ totalStorageBytes,
119
+ healthStatus,
120
+ activeAlarms: alarmCount,
121
+ costMtd: Math.round(costMtd * 100) / 100,
122
+ }),
123
+ headers: { 'Content-Type': 'application/json' },
124
+ };
125
+ } catch (err) {
126
+ logger.error('Failed to get overview', { error: String(err) });
127
+ return {
128
+ statusCode: 500,
129
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get overview' }),
130
+ headers: { 'Content-Type': 'application/json' },
131
+ };
132
+ }
133
+ }
@@ -0,0 +1,10 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ interface RouteResult {
3
+ statusCode: number;
4
+ body: string;
5
+ headers: Record<string, string>;
6
+ }
7
+ export declare function handleListTenants(page: number, ddb: DynamoDBDocumentClient, tableName: string): Promise<RouteResult>;
8
+ export declare function handleGetTenantDetail(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<RouteResult>;
9
+ export {};
10
+ //# sourceMappingURL=tenants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenants.d.ts","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA6B,MAAM,uBAAuB,CAAC;AAG1F,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAqDtB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CA2DtB"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleListTenants = handleListTenants;
4
+ exports.handleGetTenantDetail = handleGetTenantDetail;
5
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
6
+ const logger_js_1 = require("../../handler/logger.js");
7
+ const PAGE_SIZE = 25;
8
+ async function handleListTenants(page, ddb, tableName) {
9
+ try {
10
+ const validPage = Math.max(1, page);
11
+ // Scan for all META records to get tenant list
12
+ const allTenants = [];
13
+ let exclusiveStartKey;
14
+ do {
15
+ const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
16
+ TableName: tableName,
17
+ FilterExpression: 'SK = :sk',
18
+ ExpressionAttributeValues: { ':sk': 'META' },
19
+ ProjectionExpression: 'PK, publicKey, createdAt, updatedAt, storageUsedBytes',
20
+ ExclusiveStartKey: exclusiveStartKey,
21
+ }));
22
+ if (result.Items) {
23
+ allTenants.push(...result.Items);
24
+ }
25
+ exclusiveStartKey = result.LastEvaluatedKey;
26
+ } while (exclusiveStartKey);
27
+ const total = allTenants.length;
28
+ const startIndex = (validPage - 1) * PAGE_SIZE;
29
+ const paginatedTenants = allTenants.slice(startIndex, startIndex + PAGE_SIZE).map((item) => ({
30
+ tenantId: item['PK'].replace('TENANT#', ''),
31
+ publicKey: item['publicKey'],
32
+ createdAt: item['createdAt'],
33
+ updatedAt: item['updatedAt'],
34
+ storageUsedBytes: item['storageUsedBytes'] ?? 0,
35
+ }));
36
+ return {
37
+ statusCode: 200,
38
+ body: JSON.stringify({
39
+ tenants: paginatedTenants,
40
+ total,
41
+ page: validPage,
42
+ pageSize: PAGE_SIZE,
43
+ }),
44
+ headers: { 'Content-Type': 'application/json' },
45
+ };
46
+ }
47
+ catch (err) {
48
+ logger_js_1.logger.error('Failed to list tenants', { error: String(err) });
49
+ return {
50
+ statusCode: 500,
51
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to list tenants' }),
52
+ headers: { 'Content-Type': 'application/json' },
53
+ };
54
+ }
55
+ }
56
+ async function handleGetTenantDetail(tenantId, ddb, tableName) {
57
+ try {
58
+ // Query for the META record
59
+ const metaResult = await ddb.send(new lib_dynamodb_1.QueryCommand({
60
+ TableName: tableName,
61
+ KeyConditionExpression: 'PK = :pk AND SK = :sk',
62
+ ExpressionAttributeValues: {
63
+ ':pk': `TENANT#${tenantId}`,
64
+ ':sk': 'META',
65
+ },
66
+ }));
67
+ if (!metaResult.Items || metaResult.Items.length === 0) {
68
+ return {
69
+ statusCode: 404,
70
+ body: JSON.stringify({ error: 'not_found', message: 'Tenant not found' }),
71
+ headers: { 'Content-Type': 'application/json' },
72
+ };
73
+ }
74
+ const meta = metaResult.Items[0];
75
+ // Count blobs for this tenant
76
+ const blobResult = await ddb.send(new lib_dynamodb_1.QueryCommand({
77
+ TableName: tableName,
78
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
79
+ ExpressionAttributeValues: {
80
+ ':pk': `TENANT#${tenantId}`,
81
+ ':sk': 'BLOB#',
82
+ },
83
+ Select: 'COUNT',
84
+ }));
85
+ const blobCount = blobResult.Count ?? 0;
86
+ return {
87
+ statusCode: 200,
88
+ body: JSON.stringify({
89
+ tenantId,
90
+ publicKey: meta['publicKey'],
91
+ createdAt: meta['createdAt'],
92
+ updatedAt: meta['updatedAt'],
93
+ storageUsedBytes: meta['storageUsedBytes'] ?? 0,
94
+ blobCount,
95
+ }),
96
+ headers: { 'Content-Type': 'application/json' },
97
+ };
98
+ }
99
+ catch (err) {
100
+ logger_js_1.logger.error('Failed to get tenant detail', { error: String(err) });
101
+ return {
102
+ statusCode: 500,
103
+ body: JSON.stringify({ error: 'internal_error', message: 'Failed to get tenant detail' }),
104
+ headers: { 'Content-Type': 'application/json' },
105
+ };
106
+ }
107
+ }
108
+ //# sourceMappingURL=tenants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenants.js","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/tenants.ts"],"names":[],"mappings":"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"}