@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,363 @@
1
+ import * as crypto from 'crypto';
2
+ import { DynamoDBDocumentClient, PutCommand, GetCommand, DeleteCommand } from '@aws-sdk/lib-dynamodb';
3
+ import { logAuditEvent } from './audit.js';
4
+ import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
5
+ import { logger } from '../logger.js';
6
+ import {
7
+ verifyKeyOnGitHub,
8
+ storeGitHubAssociation,
9
+ storeGitHubReverseLookup,
10
+ findTenantByGitHub,
11
+ GitHubVerificationError,
12
+ } from './github.js';
13
+
14
+ interface RegisterRequest {
15
+ publicKey: string;
16
+ signedChallenge: string;
17
+ challengeNonce: string;
18
+ github?: string;
19
+ }
20
+
21
+ interface HandlerResponse {
22
+ statusCode: number;
23
+ body: string;
24
+ headers: Record<string, string>;
25
+ }
26
+
27
+ const CHALLENGE_EXPIRY_SECONDS = 60;
28
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
29
+
30
+ let cachedSignupsEnabled: { value: boolean; expiresAt: number } | null = null;
31
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
32
+
33
+ const ssmClient = new SSMClient({});
34
+
35
+ export async function checkSignupsEnabled(paramName: string): Promise<boolean> {
36
+ const now = Date.now();
37
+ if (cachedSignupsEnabled && now < cachedSignupsEnabled.expiresAt) {
38
+ return cachedSignupsEnabled.value;
39
+ }
40
+
41
+ try {
42
+ const result = await ssmClient.send(
43
+ new GetParameterCommand({ Name: paramName }),
44
+ );
45
+ const value = result.Parameter?.Value !== 'false';
46
+ cachedSignupsEnabled = { value, expiresAt: now + CACHE_TTL_MS };
47
+ return value;
48
+ } catch (err) {
49
+ logger.error('Failed to fetch signups-enabled parameter', { error: String(err) });
50
+ // Default to enabled if parameter fetch fails
51
+ return true;
52
+ }
53
+ }
54
+
55
+ // Exported for testing
56
+ export function _resetSignupsCache(): void {
57
+ cachedSignupsEnabled = null;
58
+ }
59
+
60
+ function tenantIdFromPublicKey(publicKeyBase64: string): string {
61
+ const hash = crypto.createHash('sha256').update(publicKeyBase64).digest('hex');
62
+ return hash.slice(0, 32);
63
+ }
64
+
65
+ function isValidSSHPublicKey(publicKey: string): boolean {
66
+ // Basic validation: must be base64 and reasonable length
67
+ if (!publicKey || publicKey.length < 16 || publicKey.length > 2048) {
68
+ return false;
69
+ }
70
+ try {
71
+ const decoded = Buffer.from(publicKey, 'base64');
72
+ return decoded.length > 0 && publicKey === decoded.toString('base64');
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Verify an Ed25519 signature of a challenge nonce against a public key.
80
+ * The signed data is: "chaoskb-register\n" + nonce (base64).
81
+ */
82
+ function verifyRegistrationSignature(
83
+ publicKeyBase64: string,
84
+ nonce: string,
85
+ signatureBase64: string,
86
+ ): boolean {
87
+ try {
88
+ const publicKeyBuffer = Buffer.from(publicKeyBase64, 'base64');
89
+ const signatureBuffer = Buffer.from(signatureBase64, 'base64');
90
+ const data = Buffer.from(`chaoskb-register\n${nonce}`);
91
+
92
+ const keyObject = crypto.createPublicKey({
93
+ key: Buffer.concat([
94
+ // Ed25519 DER prefix for a 32-byte public key
95
+ Buffer.from('302a300506032b6570032100', 'hex'),
96
+ publicKeyBuffer,
97
+ ]),
98
+ format: 'der',
99
+ type: 'spki',
100
+ });
101
+
102
+ return crypto.verify(null, data, keyObject, signatureBuffer);
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * GET /v1/register/challenge — generate a registration challenge nonce.
110
+ *
111
+ * Returns a 32-byte random nonce (base64-encoded) that must be signed by the
112
+ * client's SSH private key and submitted with the registration request.
113
+ * Challenge expires after 60 seconds and is single-use.
114
+ */
115
+ export async function handleChallenge(
116
+ ddb: DynamoDBDocumentClient,
117
+ tableName: string,
118
+ ): Promise<HandlerResponse> {
119
+ const nonce = crypto.randomBytes(32).toString('base64');
120
+ const now = Math.floor(Date.now() / 1000);
121
+ const ttl = now + CHALLENGE_EXPIRY_SECONDS + 60; // DynamoDB TTL: generous buffer
122
+ const expiresAt = new Date((now + CHALLENGE_EXPIRY_SECONDS) * 1000).toISOString();
123
+
124
+ await ddb.send(
125
+ new PutCommand({
126
+ TableName: tableName,
127
+ Item: {
128
+ PK: `CHALLENGE#${nonce}`,
129
+ SK: 'META',
130
+ expiresAt,
131
+ ttl,
132
+ },
133
+ }),
134
+ );
135
+
136
+ logger.info('Registration challenge created');
137
+
138
+ return {
139
+ statusCode: 200,
140
+ headers: JSON_HEADERS,
141
+ body: JSON.stringify({ challenge: nonce, expiresAt }),
142
+ };
143
+ }
144
+
145
+ export async function handleRegister(
146
+ body: string | null | undefined,
147
+ ddb: DynamoDBDocumentClient,
148
+ tableName: string,
149
+ signupsParamName: string,
150
+ ): Promise<HandlerResponse> {
151
+ // Check if signups are enabled
152
+ const signupsEnabled = await checkSignupsEnabled(signupsParamName);
153
+ if (!signupsEnabled) {
154
+ return {
155
+ statusCode: 403,
156
+ headers: JSON_HEADERS,
157
+ body: JSON.stringify({ error: 'signups_disabled', message: 'New registrations are currently disabled' }),
158
+ };
159
+ }
160
+
161
+ // Parse and validate request body
162
+ if (!body) {
163
+ return {
164
+ statusCode: 400,
165
+ headers: JSON_HEADERS,
166
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
167
+ };
168
+ }
169
+
170
+ let request: RegisterRequest;
171
+ try {
172
+ request = JSON.parse(body);
173
+ } catch {
174
+ return {
175
+ statusCode: 400,
176
+ headers: JSON_HEADERS,
177
+ body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
178
+ };
179
+ }
180
+
181
+ if (!request.publicKey) {
182
+ return {
183
+ statusCode: 400,
184
+ headers: JSON_HEADERS,
185
+ body: JSON.stringify({ error: 'invalid_request', message: 'publicKey is required' }),
186
+ };
187
+ }
188
+
189
+ if (!request.signedChallenge || !request.challengeNonce) {
190
+ return {
191
+ statusCode: 400,
192
+ headers: JSON_HEADERS,
193
+ body: JSON.stringify({ error: 'invalid_request', message: 'signedChallenge and challengeNonce are required' }),
194
+ };
195
+ }
196
+
197
+ if (!isValidSSHPublicKey(request.publicKey)) {
198
+ return {
199
+ statusCode: 400,
200
+ headers: JSON_HEADERS,
201
+ body: JSON.stringify({ error: 'invalid_request', message: 'Invalid SSH public key format' }),
202
+ };
203
+ }
204
+
205
+ // Look up and consume the challenge nonce (single-use)
206
+ const challengeResult = await ddb.send(
207
+ new GetCommand({
208
+ TableName: tableName,
209
+ Key: {
210
+ PK: `CHALLENGE#${request.challengeNonce}`,
211
+ SK: 'META',
212
+ },
213
+ }),
214
+ );
215
+
216
+ if (!challengeResult.Item) {
217
+ return {
218
+ statusCode: 400,
219
+ headers: JSON_HEADERS,
220
+ body: JSON.stringify({ error: 'invalid_challenge', message: 'Challenge not found or already used' }),
221
+ };
222
+ }
223
+
224
+ // Check challenge expiry
225
+ if (new Date(challengeResult.Item['expiresAt'] as string) < new Date()) {
226
+ // Clean up expired challenge
227
+ await ddb.send(
228
+ new DeleteCommand({
229
+ TableName: tableName,
230
+ Key: { PK: `CHALLENGE#${request.challengeNonce}`, SK: 'META' },
231
+ }),
232
+ );
233
+ return {
234
+ statusCode: 400,
235
+ headers: JSON_HEADERS,
236
+ body: JSON.stringify({ error: 'challenge_expired', message: 'Challenge has expired' }),
237
+ };
238
+ }
239
+
240
+ // Consume the challenge (delete it — single-use)
241
+ await ddb.send(
242
+ new DeleteCommand({
243
+ TableName: tableName,
244
+ Key: { PK: `CHALLENGE#${request.challengeNonce}`, SK: 'META' },
245
+ }),
246
+ );
247
+
248
+ // Verify the SSH signature of the challenge nonce against the public key
249
+ const validSignature = verifyRegistrationSignature(
250
+ request.publicKey,
251
+ request.challengeNonce,
252
+ request.signedChallenge,
253
+ );
254
+
255
+ if (!validSignature) {
256
+ logger.warn('Registration signature verification failed');
257
+ return {
258
+ statusCode: 401,
259
+ headers: JSON_HEADERS,
260
+ body: JSON.stringify({ error: 'invalid_signature', message: 'Challenge signature verification failed' }),
261
+ };
262
+ }
263
+
264
+ // GitHub verification (if --github was provided)
265
+ if (request.github) {
266
+ try {
267
+ const keyOnGitHub = await verifyKeyOnGitHub(request.publicKey, request.github);
268
+ if (!keyOnGitHub) {
269
+ return {
270
+ statusCode: 400,
271
+ headers: JSON_HEADERS,
272
+ body: JSON.stringify({
273
+ error: 'github_key_not_found',
274
+ message: `Public key not found on GitHub account "${request.github}"`,
275
+ }),
276
+ };
277
+ }
278
+ } catch (err) {
279
+ if (err instanceof GitHubVerificationError) {
280
+ return {
281
+ statusCode: 400,
282
+ headers: JSON_HEADERS,
283
+ body: JSON.stringify({ error: err.code, message: err.message }),
284
+ };
285
+ }
286
+ throw err;
287
+ }
288
+
289
+ // Check if an existing tenant is associated with this GitHub username (auto-link)
290
+ const existingTenantId = await findTenantByGitHub(request.github, ddb, tableName);
291
+ if (existingTenantId) {
292
+ logger.info('GitHub auto-link: existing tenant found', {
293
+ existingTenantId,
294
+ github: request.github,
295
+ });
296
+ return {
297
+ statusCode: 200,
298
+ headers: JSON_HEADERS,
299
+ body: JSON.stringify({
300
+ status: 'auto_linked',
301
+ tenantId: existingTenantId,
302
+ github: request.github,
303
+ }),
304
+ };
305
+ }
306
+ }
307
+
308
+ const tenantId = tenantIdFromPublicKey(request.publicKey);
309
+ const now = new Date().toISOString();
310
+
311
+ try {
312
+ await ddb.send(
313
+ new PutCommand({
314
+ TableName: tableName,
315
+ Item: {
316
+ PK: `TENANT#${tenantId}`,
317
+ SK: 'META',
318
+ publicKey: request.publicKey,
319
+ createdAt: now,
320
+ updatedAt: now,
321
+ storageUsedBytes: 0,
322
+ },
323
+ ConditionExpression: 'attribute_not_exists(SK)',
324
+ }),
325
+ );
326
+
327
+ logger.info('Tenant registered', { tenantId, operation: 'register' });
328
+
329
+ // Store GitHub association if provided
330
+ if (request.github) {
331
+ await storeGitHubAssociation(tenantId, request.github, ddb, tableName);
332
+ await storeGitHubReverseLookup(request.github, tenantId, ddb, tableName);
333
+ }
334
+
335
+ await logAuditEvent(ddb, tableName, tenantId, {
336
+ eventType: 'registered',
337
+ fingerprint: '',
338
+ metadata: {
339
+ publicKey: request.publicKey,
340
+ ...(request.github && { github: request.github }),
341
+ },
342
+ });
343
+
344
+ return {
345
+ statusCode: 201,
346
+ headers: JSON_HEADERS,
347
+ body: JSON.stringify({
348
+ tenantId,
349
+ publicKey: request.publicKey,
350
+ ...(request.github && { github: request.github }),
351
+ }),
352
+ };
353
+ } catch (err: unknown) {
354
+ if ((err as { name?: string }).name === 'ConditionalCheckFailedException') {
355
+ return {
356
+ statusCode: 409,
357
+ headers: JSON_HEADERS,
358
+ body: JSON.stringify({ error: 'already_registered', message: 'This public key is already registered' }),
359
+ };
360
+ }
361
+ throw err;
362
+ }
363
+ }
@@ -0,0 +1,9 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ interface HandlerResponse {
3
+ statusCode: number;
4
+ body: string;
5
+ headers: Record<string, string>;
6
+ }
7
+ export declare function handleRestore(blobId: string, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
8
+ export {};
9
+ //# sourceMappingURL=restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA6B,MAAM,uBAAuB,CAAC;AAG1F,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA0D1B"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleRestore = handleRestore;
4
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
+ const logger_js_1 = require("../logger.js");
6
+ async function handleRestore(blobId, tenantId, ddb, tableName) {
7
+ // Get the blob to verify it exists and is deleted
8
+ const existing = await ddb.send(new lib_dynamodb_1.GetCommand({
9
+ TableName: tableName,
10
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
11
+ ProjectionExpression: 'deletedAt, #s',
12
+ ExpressionAttributeNames: { '#s': 'size' },
13
+ }));
14
+ if (!existing.Item) {
15
+ return {
16
+ statusCode: 404,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
19
+ };
20
+ }
21
+ if (!existing.Item['deletedAt']) {
22
+ return {
23
+ statusCode: 400,
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ error: 'not_deleted', message: 'Blob is not deleted' }),
26
+ };
27
+ }
28
+ const blobSize = existing.Item['size'] ?? 0;
29
+ const now = new Date().toISOString();
30
+ // Remove deletedAt and ttl
31
+ await ddb.send(new lib_dynamodb_1.UpdateCommand({
32
+ TableName: tableName,
33
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
34
+ UpdateExpression: 'REMOVE deletedAt, #ttl SET updatedAt = :updatedAt',
35
+ ExpressionAttributeNames: { '#ttl': 'ttl' },
36
+ ExpressionAttributeValues: { ':updatedAt': now },
37
+ }));
38
+ // Re-increment storage used
39
+ await ddb.send(new lib_dynamodb_1.UpdateCommand({
40
+ TableName: tableName,
41
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
42
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes + :size',
43
+ ExpressionAttributeValues: { ':size': blobSize },
44
+ }));
45
+ logger_js_1.logger.info('Blob restored', { tenantId, operation: 'RESTORE' });
46
+ return {
47
+ statusCode: 200,
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ id: blobId, restored: true }),
50
+ };
51
+ }
52
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.js","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":";;AASA,sCA+DC;AAxED,wDAA0F;AAC1F,4CAAsC;AAQ/B,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,QAAgB,EAChB,GAA2B,EAC3B,SAAiB;IAEjB,kDAAkD;IAClD,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,CAC7B,IAAI,yBAAU,CAAC;QACb,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,MAAM,EAAE,EAAE;QACvD,oBAAoB,EAAE,eAAe;QACrC,wBAAwB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;KAC3C,CAAC,CACH,CAAC;IAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;SACxE,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,OAAO;YACL,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;SAC/E,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAY,IAAI,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,2BAA2B;IAC3B,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,4BAAa,CAAC;QAChB,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,MAAM,EAAE,EAAE;QACvD,gBAAgB,EAAE,mDAAmD;QACrE,wBAAwB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QAC3C,yBAAyB,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE;KACjD,CAAC,CACH,CAAC;IAEF,4BAA4B;IAC5B,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,4BAAa,CAAC;QAChB,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;QAC7C,gBAAgB,EAAE,iDAAiD;QACnE,yBAAyB,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE;KACjD,CAAC,CACH,CAAC;IAEF,kBAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;IAEjE,OAAO;QACL,UAAU,EAAE,GAAG;QACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KACrD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,73 @@
1
+ import { DynamoDBDocumentClient, UpdateCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
2
+ import { logger } from '../logger.js';
3
+
4
+ interface HandlerResponse {
5
+ statusCode: number;
6
+ body: string;
7
+ headers: Record<string, string>;
8
+ }
9
+
10
+ export async function handleRestore(
11
+ blobId: string,
12
+ tenantId: string,
13
+ ddb: DynamoDBDocumentClient,
14
+ tableName: string,
15
+ ): Promise<HandlerResponse> {
16
+ // Get the blob to verify it exists and is deleted
17
+ const existing = await ddb.send(
18
+ new GetCommand({
19
+ TableName: tableName,
20
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
21
+ ProjectionExpression: 'deletedAt, #s',
22
+ ExpressionAttributeNames: { '#s': 'size' },
23
+ }),
24
+ );
25
+
26
+ if (!existing.Item) {
27
+ return {
28
+ statusCode: 404,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
31
+ };
32
+ }
33
+
34
+ if (!existing.Item['deletedAt']) {
35
+ return {
36
+ statusCode: 400,
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ error: 'not_deleted', message: 'Blob is not deleted' }),
39
+ };
40
+ }
41
+
42
+ const blobSize = (existing.Item['size'] as number) ?? 0;
43
+ const now = new Date().toISOString();
44
+
45
+ // Remove deletedAt and ttl
46
+ await ddb.send(
47
+ new UpdateCommand({
48
+ TableName: tableName,
49
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
50
+ UpdateExpression: 'REMOVE deletedAt, #ttl SET updatedAt = :updatedAt',
51
+ ExpressionAttributeNames: { '#ttl': 'ttl' },
52
+ ExpressionAttributeValues: { ':updatedAt': now },
53
+ }),
54
+ );
55
+
56
+ // Re-increment storage used
57
+ await ddb.send(
58
+ new UpdateCommand({
59
+ TableName: tableName,
60
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
61
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes + :size',
62
+ ExpressionAttributeValues: { ':size': blobSize },
63
+ }),
64
+ );
65
+
66
+ logger.info('Blob restored', { tenantId, operation: 'RESTORE' });
67
+
68
+ return {
69
+ statusCode: 200,
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ id: blobId, restored: true }),
72
+ };
73
+ }
@@ -0,0 +1,13 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ interface HandlerResponse {
3
+ statusCode: number;
4
+ body: string;
5
+ headers: Record<string, string>;
6
+ }
7
+ /**
8
+ * Emergency revocation: delete all KEY#, WRAPPED_KEY#, ROTATION, and SEQUENCE# items
9
+ * for a tenant, then log an audit event.
10
+ */
11
+ export declare function handleRevokeAll(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
12
+ export {};
13
+ //# sourceMappingURL=revocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/revocation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EAGvB,MAAM,uBAAuB,CAAC;AAI/B,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAgE1B"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleRevokeAll = handleRevokeAll;
4
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
+ const audit_js_1 = require("./audit.js");
6
+ const logger_js_1 = require("../logger.js");
7
+ /**
8
+ * Emergency revocation: delete all KEY#, WRAPPED_KEY#, ROTATION, and SEQUENCE# items
9
+ * for a tenant, then log an audit event.
10
+ */
11
+ async function handleRevokeAll(tenantId, ddb, tableName) {
12
+ const pk = `TENANT#${tenantId}`;
13
+ // Query all items in the tenant partition
14
+ const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
15
+ TableName: tableName,
16
+ KeyConditionExpression: 'PK = :pk',
17
+ ExpressionAttributeValues: {
18
+ ':pk': pk,
19
+ },
20
+ ProjectionExpression: 'PK, SK',
21
+ }));
22
+ const allItems = result.Items ?? [];
23
+ // Filter to revocable SK prefixes
24
+ const toDelete = allItems.filter((item) => {
25
+ const sk = item['SK'];
26
+ return (sk.startsWith('KEY#') ||
27
+ sk.startsWith('WRAPPED_KEY#') ||
28
+ sk === 'ROTATION' ||
29
+ sk.startsWith('SEQUENCE#'));
30
+ });
31
+ // Batch delete in groups of 25
32
+ for (let i = 0; i < toDelete.length; i += 25) {
33
+ const batch = toDelete.slice(i, i + 25);
34
+ await ddb.send(new lib_dynamodb_1.BatchWriteCommand({
35
+ RequestItems: {
36
+ [tableName]: batch.map((item) => ({
37
+ DeleteRequest: {
38
+ Key: { PK: item['PK'], SK: item['SK'] },
39
+ },
40
+ })),
41
+ },
42
+ }));
43
+ }
44
+ logger_js_1.logger.info('Emergency revocation completed', {
45
+ tenantId,
46
+ deletedItems: toDelete.length,
47
+ });
48
+ // Log audit event
49
+ await (0, audit_js_1.logAuditEvent)(ddb, tableName, tenantId, {
50
+ eventType: 'revoked',
51
+ fingerprint: 'all',
52
+ metadata: { deletedItems: toDelete.length },
53
+ });
54
+ return {
55
+ statusCode: 200,
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({
58
+ status: 'revoked',
59
+ message: 'All devices revoked. Re-register with a new SSH key.',
60
+ }),
61
+ };
62
+ }
63
+ //# sourceMappingURL=revocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revocation.js","sourceRoot":"","sources":["../../../../lib/handler/routes/revocation.ts"],"names":[],"mappings":";;AAkBA,0CAoEC;AAtFD,wDAI+B;AAC/B,yCAA2C;AAC3C,4CAAsC;AAQtC;;;GAGG;AACI,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,GAA2B,EAC3B,SAAiB;IAEjB,MAAM,EAAE,GAAG,UAAU,QAAQ,EAAE,CAAC;IAEhC,0CAA0C;IAC1C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAC3B,IAAI,2BAAY,CAAC;QACf,SAAS,EAAE,SAAS;QACpB,sBAAsB,EAAE,UAAU;QAClC,yBAAyB,EAAE;YACzB,KAAK,EAAE,EAAE;SACV;QACD,oBAAoB,EAAE,QAAQ;KAC/B,CAAC,CACH,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;IAEpC,kCAAkC;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QACxC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAW,CAAC;QAChC,OAAO,CACL,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;YACrB,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC;YAC7B,EAAE,KAAK,UAAU;YACjB,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,+BAA+B;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QACxC,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,gCAAiB,CAAC;YACpB,YAAY,EAAE;gBACZ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;oBAChC,aAAa,EAAE;wBACb,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;qBACxC;iBACF,CAAC,CAAC;aACJ;SACF,CAAC,CACH,CAAC;IACJ,CAAC;IAED,kBAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;QAC5C,QAAQ;QACR,YAAY,EAAE,QAAQ,CAAC,MAAM;KAC9B,CAAC,CAAC;IAEH,kBAAkB;IAClB,MAAM,IAAA,wBAAa,EAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE;QAC5C,SAAS,EAAE,SAAS;QACpB,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,EAAE,YAAY,EAAE,QAAQ,CAAC,MAAM,EAAE;KAC5C,CAAC,CAAC;IAEH,OAAO;QACL,UAAU,EAAE,GAAG;QACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,sDAAsD;SAChE,CAAC;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,87 @@
1
+ import {
2
+ DynamoDBDocumentClient,
3
+ QueryCommand,
4
+ BatchWriteCommand,
5
+ } from '@aws-sdk/lib-dynamodb';
6
+ import { logAuditEvent } from './audit.js';
7
+ import { logger } from '../logger.js';
8
+
9
+ interface HandlerResponse {
10
+ statusCode: number;
11
+ body: string;
12
+ headers: Record<string, string>;
13
+ }
14
+
15
+ /**
16
+ * Emergency revocation: delete all KEY#, WRAPPED_KEY#, ROTATION, and SEQUENCE# items
17
+ * for a tenant, then log an audit event.
18
+ */
19
+ export async function handleRevokeAll(
20
+ tenantId: string,
21
+ ddb: DynamoDBDocumentClient,
22
+ tableName: string,
23
+ ): Promise<HandlerResponse> {
24
+ const pk = `TENANT#${tenantId}`;
25
+
26
+ // Query all items in the tenant partition
27
+ const result = await ddb.send(
28
+ new QueryCommand({
29
+ TableName: tableName,
30
+ KeyConditionExpression: 'PK = :pk',
31
+ ExpressionAttributeValues: {
32
+ ':pk': pk,
33
+ },
34
+ ProjectionExpression: 'PK, SK',
35
+ }),
36
+ );
37
+
38
+ const allItems = result.Items ?? [];
39
+
40
+ // Filter to revocable SK prefixes
41
+ const toDelete = allItems.filter((item) => {
42
+ const sk = item['SK'] as string;
43
+ return (
44
+ sk.startsWith('KEY#') ||
45
+ sk.startsWith('WRAPPED_KEY#') ||
46
+ sk === 'ROTATION' ||
47
+ sk.startsWith('SEQUENCE#')
48
+ );
49
+ });
50
+
51
+ // Batch delete in groups of 25
52
+ for (let i = 0; i < toDelete.length; i += 25) {
53
+ const batch = toDelete.slice(i, i + 25);
54
+ await ddb.send(
55
+ new BatchWriteCommand({
56
+ RequestItems: {
57
+ [tableName]: batch.map((item) => ({
58
+ DeleteRequest: {
59
+ Key: { PK: item['PK'], SK: item['SK'] },
60
+ },
61
+ })),
62
+ },
63
+ }),
64
+ );
65
+ }
66
+
67
+ logger.info('Emergency revocation completed', {
68
+ tenantId,
69
+ deletedItems: toDelete.length,
70
+ });
71
+
72
+ // Log audit event
73
+ await logAuditEvent(ddb, tableName, tenantId, {
74
+ eventType: 'revoked',
75
+ fingerprint: 'all',
76
+ metadata: { deletedItems: toDelete.length },
77
+ });
78
+
79
+ return {
80
+ statusCode: 200,
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({
83
+ status: 'revoked',
84
+ message: 'All devices revoked. Re-register with a new SSH key.',
85
+ }),
86
+ };
87
+ }