@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,458 @@
1
+ import * as crypto from 'crypto';
2
+ import {
3
+ DynamoDBDocumentClient,
4
+ PutCommand,
5
+ GetCommand,
6
+ DeleteCommand,
7
+ QueryCommand,
8
+ UpdateCommand,
9
+ } from '@aws-sdk/lib-dynamodb';
10
+ import { logger } from '../logger.js';
11
+ import { createNotification } from './notifications.js';
12
+
13
+ interface HandlerResponse {
14
+ statusCode: number;
15
+ body: string;
16
+ headers: Record<string, string>;
17
+ }
18
+
19
+ const JSON_HEADERS = { 'Content-Type': 'application/json' };
20
+
21
+ /**
22
+ * POST /v1/link-code — authenticated
23
+ *
24
+ * Creates a link code record so a new device can join this tenant.
25
+ * The caller displays the raw code; the server stores only its SHA-256 hash.
26
+ */
27
+ export async function handleCreateLinkCode(
28
+ tenantId: string,
29
+ body: string | null | undefined,
30
+ ddb: DynamoDBDocumentClient,
31
+ tableName: string,
32
+ ): Promise<HandlerResponse> {
33
+ if (!body) {
34
+ return {
35
+ statusCode: 400,
36
+ headers: JSON_HEADERS,
37
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
38
+ };
39
+ }
40
+
41
+ let parsed: { codeHash: string };
42
+ try {
43
+ parsed = JSON.parse(body);
44
+ } catch {
45
+ return {
46
+ statusCode: 400,
47
+ headers: JSON_HEADERS,
48
+ body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
49
+ };
50
+ }
51
+
52
+ if (!parsed.codeHash || typeof parsed.codeHash !== 'string') {
53
+ return {
54
+ statusCode: 400,
55
+ headers: JSON_HEADERS,
56
+ body: JSON.stringify({ error: 'invalid_request', message: 'codeHash is required' }),
57
+ };
58
+ }
59
+
60
+ const now = new Date();
61
+ const expiresAt = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
62
+ const ttl = Math.floor(now.getTime() / 1000) + 10 * 60; // DynamoDB TTL: 10 min (generous)
63
+
64
+ await ddb.send(
65
+ new PutCommand({
66
+ TableName: tableName,
67
+ Item: {
68
+ PK: `TENANT#${tenantId}`,
69
+ SK: `LINK#${parsed.codeHash}`,
70
+ newPublicKey: null,
71
+ failureCount: 0,
72
+ expiresAt,
73
+ ttl,
74
+ },
75
+ }),
76
+ );
77
+
78
+ logger.info('Link code created', { tenantId });
79
+
80
+ return {
81
+ statusCode: 201,
82
+ headers: JSON_HEADERS,
83
+ body: JSON.stringify({ status: 'created', expiresAt }),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * POST /v1/link-confirm — unauthenticated (new device)
89
+ *
90
+ * The new device sends the raw link code and its public key.
91
+ * We hash the code, look up the LINK record, validate expiry and failure count,
92
+ * and store the public key for the existing device to pick up.
93
+ */
94
+ export async function handleLinkConfirm(
95
+ body: string | null | undefined,
96
+ ddb: DynamoDBDocumentClient,
97
+ tableName: string,
98
+ ): Promise<HandlerResponse> {
99
+ if (!body) {
100
+ return {
101
+ statusCode: 400,
102
+ headers: JSON_HEADERS,
103
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
104
+ };
105
+ }
106
+
107
+ let parsed: { linkCode: string; publicKey: string };
108
+ try {
109
+ parsed = JSON.parse(body);
110
+ } catch {
111
+ return {
112
+ statusCode: 400,
113
+ headers: JSON_HEADERS,
114
+ body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
115
+ };
116
+ }
117
+
118
+ if (!parsed.linkCode || !parsed.publicKey) {
119
+ return {
120
+ statusCode: 400,
121
+ headers: JSON_HEADERS,
122
+ body: JSON.stringify({ error: 'invalid_request', message: 'linkCode and publicKey are required' }),
123
+ };
124
+ }
125
+
126
+ const codeHash = crypto.createHash('sha256').update(parsed.linkCode).digest('hex');
127
+
128
+ // Find the LINK record across tenants — query by SK pattern
129
+ // We need to scan for the link code. Since link codes are short-lived and rare,
130
+ // we use a GSI or scan. For simplicity, the caller must provide tenantId or
131
+ // we search by a known pattern. Actually, the link code hash is unique enough
132
+ // that we store a reverse-lookup record.
133
+ //
134
+ // Alternative approach: query all tenants. But DynamoDB doesn't support that
135
+ // efficiently. Instead, store a top-level LINK_CODE#{hash} -> tenantId mapping.
136
+ //
137
+ // For this implementation, the link-confirm looks up LINK_CODE#{hash} at the
138
+ // table level (PK = LINK_CODE#{hash}).
139
+ const lookupResult = await ddb.send(
140
+ new GetCommand({
141
+ TableName: tableName,
142
+ Key: {
143
+ PK: `LINK_CODE#${codeHash}`,
144
+ SK: 'META',
145
+ },
146
+ }),
147
+ );
148
+
149
+ // Fallback: try the tenant-scoped approach by scanning LINK# records
150
+ // For the initial implementation, we use a top-level lookup key.
151
+ // The handleCreateLinkCode also writes this lookup record.
152
+
153
+ if (!lookupResult.Item) {
154
+ return {
155
+ statusCode: 404,
156
+ headers: JSON_HEADERS,
157
+ body: JSON.stringify({ error: 'not_found', message: 'Invalid or expired link code' }),
158
+ };
159
+ }
160
+
161
+ const tenantId = lookupResult.Item['tenantId'] as string;
162
+
163
+ // Fetch the tenant-scoped link record
164
+ const linkResult = await ddb.send(
165
+ new GetCommand({
166
+ TableName: tableName,
167
+ Key: {
168
+ PK: `TENANT#${tenantId}`,
169
+ SK: `LINK#${codeHash}`,
170
+ },
171
+ }),
172
+ );
173
+
174
+ if (!linkResult.Item) {
175
+ return {
176
+ statusCode: 404,
177
+ headers: JSON_HEADERS,
178
+ body: JSON.stringify({ error: 'not_found', message: 'Invalid or expired link code' }),
179
+ };
180
+ }
181
+
182
+ const linkRecord = linkResult.Item;
183
+
184
+ // Check expiry
185
+ if (new Date(linkRecord['expiresAt'] as string) < new Date()) {
186
+ return {
187
+ statusCode: 410,
188
+ headers: JSON_HEADERS,
189
+ body: JSON.stringify({ error: 'expired', message: 'Link code has expired' }),
190
+ };
191
+ }
192
+
193
+ // Check failure count
194
+ const failureCount = (linkRecord['failureCount'] as number) ?? 0;
195
+ if (failureCount >= 3) {
196
+ // Delete the record
197
+ await ddb.send(
198
+ new DeleteCommand({
199
+ TableName: tableName,
200
+ Key: { PK: `TENANT#${tenantId}`, SK: `LINK#${codeHash}` },
201
+ }),
202
+ );
203
+ await ddb.send(
204
+ new DeleteCommand({
205
+ TableName: tableName,
206
+ Key: { PK: `LINK_CODE#${codeHash}`, SK: 'META' },
207
+ }),
208
+ );
209
+ return {
210
+ statusCode: 429,
211
+ headers: JSON_HEADERS,
212
+ body: JSON.stringify({ error: 'too_many_failures', message: 'Too many failed attempts' }),
213
+ };
214
+ }
215
+
216
+ // Store the new device's public key in the link record
217
+ try {
218
+ await ddb.send(
219
+ new UpdateCommand({
220
+ TableName: tableName,
221
+ Key: {
222
+ PK: `TENANT#${tenantId}`,
223
+ SK: `LINK#${codeHash}`,
224
+ },
225
+ UpdateExpression: 'SET newPublicKey = :pk',
226
+ ConditionExpression: 'attribute_exists(PK)',
227
+ ExpressionAttributeValues: {
228
+ ':pk': parsed.publicKey,
229
+ },
230
+ }),
231
+ );
232
+ } catch (err: unknown) {
233
+ if ((err as { name?: string }).name === 'ConditionalCheckFailedException') {
234
+ return {
235
+ statusCode: 404,
236
+ headers: JSON_HEADERS,
237
+ body: JSON.stringify({ error: 'not_found', message: 'Link code no longer valid' }),
238
+ };
239
+ }
240
+ throw err;
241
+ }
242
+
243
+ logger.info('Link confirmed', { tenantId, codeHash: codeHash.slice(0, 8) });
244
+
245
+ return {
246
+ statusCode: 200,
247
+ headers: JSON_HEADERS,
248
+ body: JSON.stringify({ status: 'confirmed' }),
249
+ };
250
+ }
251
+
252
+ /**
253
+ * POST /v1/link-code (extended) — also writes the reverse-lookup record.
254
+ *
255
+ * This is a wrapper that ensures both the tenant-scoped LINK# record
256
+ * and the top-level LINK_CODE# lookup record are created.
257
+ */
258
+ export async function handleCreateLinkCodeFull(
259
+ tenantId: string,
260
+ body: string | null | undefined,
261
+ ddb: DynamoDBDocumentClient,
262
+ tableName: string,
263
+ ): Promise<HandlerResponse> {
264
+ if (!body) {
265
+ return {
266
+ statusCode: 400,
267
+ headers: JSON_HEADERS,
268
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
269
+ };
270
+ }
271
+
272
+ let parsed: { codeHash: string };
273
+ try {
274
+ parsed = JSON.parse(body);
275
+ } catch {
276
+ return {
277
+ statusCode: 400,
278
+ headers: JSON_HEADERS,
279
+ body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
280
+ };
281
+ }
282
+
283
+ if (!parsed.codeHash || typeof parsed.codeHash !== 'string') {
284
+ return {
285
+ statusCode: 400,
286
+ headers: JSON_HEADERS,
287
+ body: JSON.stringify({ error: 'invalid_request', message: 'codeHash is required' }),
288
+ };
289
+ }
290
+
291
+ const now = new Date();
292
+ const expiresAt = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
293
+ const ttl = Math.floor(now.getTime() / 1000) + 10 * 60;
294
+
295
+ // Write the tenant-scoped link record
296
+ await ddb.send(
297
+ new PutCommand({
298
+ TableName: tableName,
299
+ Item: {
300
+ PK: `TENANT#${tenantId}`,
301
+ SK: `LINK#${parsed.codeHash}`,
302
+ newPublicKey: null,
303
+ failureCount: 0,
304
+ expiresAt,
305
+ ttl,
306
+ },
307
+ }),
308
+ );
309
+
310
+ // Write the reverse-lookup record (for unauthenticated link-confirm)
311
+ await ddb.send(
312
+ new PutCommand({
313
+ TableName: tableName,
314
+ Item: {
315
+ PK: `LINK_CODE#${parsed.codeHash}`,
316
+ SK: 'META',
317
+ tenantId,
318
+ expiresAt,
319
+ ttl,
320
+ },
321
+ }),
322
+ );
323
+
324
+ logger.info('Link code created', { tenantId });
325
+
326
+ return {
327
+ statusCode: 201,
328
+ headers: JSON_HEADERS,
329
+ body: JSON.stringify({ status: 'created', expiresAt }),
330
+ };
331
+ }
332
+
333
+ /**
334
+ * GET /v1/link-code/{hash}/status — authenticated
335
+ *
336
+ * Returns { status: 'waiting' } or { status: 'ready', newPublicKey }.
337
+ */
338
+ export async function handleGetLinkCodeStatus(
339
+ tenantId: string,
340
+ codeHash: string,
341
+ ddb: DynamoDBDocumentClient,
342
+ tableName: string,
343
+ ): Promise<HandlerResponse> {
344
+ const result = await ddb.send(
345
+ new GetCommand({
346
+ TableName: tableName,
347
+ Key: {
348
+ PK: `TENANT#${tenantId}`,
349
+ SK: `LINK#${codeHash}`,
350
+ },
351
+ }),
352
+ );
353
+
354
+ if (!result.Item) {
355
+ return {
356
+ statusCode: 404,
357
+ headers: JSON_HEADERS,
358
+ body: JSON.stringify({ error: 'not_found', message: 'Link code not found' }),
359
+ };
360
+ }
361
+
362
+ const newPublicKey = result.Item['newPublicKey'] as string | null;
363
+
364
+ if (newPublicKey) {
365
+ return {
366
+ statusCode: 200,
367
+ headers: JSON_HEADERS,
368
+ body: JSON.stringify({ status: 'ready', newPublicKey }),
369
+ };
370
+ }
371
+
372
+ return {
373
+ statusCode: 200,
374
+ headers: JSON_HEADERS,
375
+ body: JSON.stringify({ status: 'waiting' }),
376
+ };
377
+ }
378
+
379
+ /**
380
+ * GET /v1/devices — authenticated
381
+ *
382
+ * Lists all registered devices (KEY#{fingerprint} items) for the tenant.
383
+ */
384
+ export async function handleListDevices(
385
+ tenantId: string,
386
+ ddb: DynamoDBDocumentClient,
387
+ tableName: string,
388
+ ): Promise<HandlerResponse> {
389
+ const result = await ddb.send(
390
+ new QueryCommand({
391
+ TableName: tableName,
392
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
393
+ ExpressionAttributeValues: {
394
+ ':pk': `TENANT#${tenantId}`,
395
+ ':prefix': 'KEY#',
396
+ },
397
+ }),
398
+ );
399
+
400
+ const devices = (result.Items ?? []).map((item) => ({
401
+ fingerprint: (item['SK'] as string).replace('KEY#', ''),
402
+ registeredAt: item['registeredAt'] as string,
403
+ publicKey: item['publicKey'] as string | undefined,
404
+ }));
405
+
406
+ return {
407
+ statusCode: 200,
408
+ headers: JSON_HEADERS,
409
+ body: JSON.stringify({ devices }),
410
+ };
411
+ }
412
+
413
+ /**
414
+ * DELETE /v1/devices/{fingerprint} — authenticated
415
+ *
416
+ * Removes the KEY# item and WRAPPED_KEY# item for the given device.
417
+ */
418
+ export async function handleDeleteDevice(
419
+ tenantId: string,
420
+ fingerprint: string,
421
+ ddb: DynamoDBDocumentClient,
422
+ tableName: string,
423
+ ): Promise<HandlerResponse> {
424
+ // Delete the KEY# record
425
+ await ddb.send(
426
+ new DeleteCommand({
427
+ TableName: tableName,
428
+ Key: {
429
+ PK: `TENANT#${tenantId}`,
430
+ SK: `KEY#${fingerprint}`,
431
+ },
432
+ }),
433
+ );
434
+
435
+ // Delete the WRAPPED_KEY# record
436
+ await ddb.send(
437
+ new DeleteCommand({
438
+ TableName: tableName,
439
+ Key: {
440
+ PK: `TENANT#${tenantId}`,
441
+ SK: `WRAPPED_KEY#${fingerprint}`,
442
+ },
443
+ }),
444
+ );
445
+
446
+ // Create revocation notification for remaining devices
447
+ await createNotification(tenantId, 'device_revoked', {
448
+ hostname: fingerprint,
449
+ }, ddb, tableName);
450
+
451
+ logger.info('Device removed', { tenantId, fingerprint });
452
+
453
+ return {
454
+ statusCode: 200,
455
+ headers: JSON_HEADERS,
456
+ body: JSON.stringify({ status: 'deleted' }),
457
+ };
458
+ }
@@ -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 handleExport(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
8
+ export {};
9
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAgB,MAAM,uBAAuB,CAAC;AAG7E,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,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAyC1B"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleExport = handleExport;
4
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
5
+ const logger_js_1 = require("../logger.js");
6
+ async function handleExport(tenantId, ddb, tableName) {
7
+ const allBlobs = [];
8
+ let lastKey;
9
+ do {
10
+ const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
11
+ TableName: tableName,
12
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
13
+ ExpressionAttributeValues: {
14
+ ':pk': `TENANT#${tenantId}`,
15
+ ':prefix': 'BLOB#',
16
+ },
17
+ FilterExpression: 'attribute_not_exists(deletedAt)',
18
+ ExclusiveStartKey: lastKey,
19
+ }));
20
+ for (const item of result.Items ?? []) {
21
+ const sk = item['SK'];
22
+ const blobId = sk.slice(5); // Remove 'BLOB#' prefix
23
+ const data = item['data'];
24
+ allBlobs.push({
25
+ id: blobId,
26
+ data: Buffer.from(data).toString('base64'),
27
+ size: item['size'],
28
+ ts: item['updatedAt'],
29
+ });
30
+ }
31
+ lastKey = result.LastEvaluatedKey;
32
+ } while (lastKey);
33
+ logger_js_1.logger.info('Export completed', { tenantId, operation: 'EXPORT', blobCount: allBlobs.length });
34
+ return {
35
+ statusCode: 200,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({ blobs: allBlobs }),
38
+ };
39
+ }
40
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../../../lib/handler/routes/export.ts"],"names":[],"mappings":";;AASA,oCA6CC;AAtDD,wDAA6E;AAC7E,4CAAsC;AAQ/B,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,GAA2B,EAC3B,SAAiB;IAEjB,MAAM,QAAQ,GAA6D,EAAE,CAAC;IAC9E,IAAI,OAA4C,CAAC;IAEjD,GAAG,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAC3B,IAAI,2BAAY,CAAC;YACf,SAAS,EAAE,SAAS;YACpB,sBAAsB,EAAE,uCAAuC;YAC/D,yBAAyB,EAAE;gBACzB,KAAK,EAAE,UAAU,QAAQ,EAAE;gBAC3B,SAAS,EAAE,OAAO;aACnB;YACD,gBAAgB,EAAE,iCAAiC;YACnD,iBAAiB,EAAE,OAAO;SAC3B,CAAC,CACH,CAAC;QAEF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YACtC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAW,CAAC;YAChC,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;YACpD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAW,CAAC;YAEpC,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,MAAM;gBACV,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC1C,IAAI,EAAE,IAAI,CAAC,MAAM,CAAW;gBAC5B,EAAE,EAAE,IAAI,CAAC,WAAW,CAAW;aAChC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,GAAG,MAAM,CAAC,gBAAuD,CAAC;IAC3E,CAAC,QAAQ,OAAO,EAAE;IAElB,kBAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAE/F,OAAO;QACL,UAAU,EAAE,GAAG;QACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;KAC1C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { DynamoDBDocumentClient, QueryCommand } 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 handleExport(
11
+ tenantId: string,
12
+ ddb: DynamoDBDocumentClient,
13
+ tableName: string,
14
+ ): Promise<HandlerResponse> {
15
+ const allBlobs: { id: string; data: string; size: number; ts: string }[] = [];
16
+ let lastKey: Record<string, unknown> | undefined;
17
+
18
+ do {
19
+ const result = await ddb.send(
20
+ new QueryCommand({
21
+ TableName: tableName,
22
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
23
+ ExpressionAttributeValues: {
24
+ ':pk': `TENANT#${tenantId}`,
25
+ ':prefix': 'BLOB#',
26
+ },
27
+ FilterExpression: 'attribute_not_exists(deletedAt)',
28
+ ExclusiveStartKey: lastKey,
29
+ }),
30
+ );
31
+
32
+ for (const item of result.Items ?? []) {
33
+ const sk = item['SK'] as string;
34
+ const blobId = sk.slice(5); // Remove 'BLOB#' prefix
35
+ const data = item['data'] as Buffer;
36
+
37
+ allBlobs.push({
38
+ id: blobId,
39
+ data: Buffer.from(data).toString('base64'),
40
+ size: item['size'] as number,
41
+ ts: item['updatedAt'] as string,
42
+ });
43
+ }
44
+
45
+ lastKey = result.LastEvaluatedKey as Record<string, unknown> | undefined;
46
+ } while (lastKey);
47
+
48
+ logger.info('Export completed', { tenantId, operation: 'EXPORT', blobCount: allBlobs.length });
49
+
50
+ return {
51
+ statusCode: 200,
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({ blobs: allBlobs }),
54
+ };
55
+ }
@@ -0,0 +1,31 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ /**
3
+ * Fetch SSH public keys from GitHub for a username.
4
+ * Returns one key per line. Uses a 5-minute in-memory cache.
5
+ */
6
+ export declare function fetchGitHubKeys(username: string): Promise<string[]>;
7
+ /**
8
+ * Verify that a public key (in SSH authorized_keys format or base64) appears
9
+ * on a GitHub account.
10
+ */
11
+ export declare function verifyKeyOnGitHub(publicKeyBase64: string, githubUsername: string): Promise<boolean>;
12
+ export declare class GitHubVerificationError extends Error {
13
+ readonly code: string;
14
+ constructor(code: string, message: string);
15
+ }
16
+ /**
17
+ * Store the GitHub username association on a tenant.
18
+ * DynamoDB: PK: TENANT#{tenantId}, SK: GITHUB#{username}
19
+ */
20
+ export declare function storeGitHubAssociation(tenantId: string, githubUsername: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<void>;
21
+ /**
22
+ * Look up if a tenant is associated with a GitHub username.
23
+ * Returns the tenant ID if found, null otherwise.
24
+ */
25
+ export declare function findTenantByGitHub(githubUsername: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<string | null>;
26
+ /**
27
+ * Store a reverse lookup record: GITHUB#{username} -> tenantId
28
+ */
29
+ export declare function storeGitHubReverseLookup(githubUsername: string, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<void>;
30
+ export declare function _resetGitHubKeyCache(): void;
31
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/github.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA0B,MAAM,uBAAuB,CAAC;AAmBvF;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA4BzE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC,CAelB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAcxB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAYf;AAGD,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}