@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,298 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.handlePutBlob = handlePutBlob;
37
+ exports.handleGetBlob = handleGetBlob;
38
+ exports.handleDeleteBlob = handleDeleteBlob;
39
+ exports.handleListBlobs = handleListBlobs;
40
+ exports.handleCountBlobs = handleCountBlobs;
41
+ const crypto = __importStar(require("crypto"));
42
+ const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
43
+ const input_validation_js_1 = require("../middleware/input-validation.js");
44
+ const rate_limit_js_1 = require("../middleware/rate-limit.js");
45
+ const logger_js_1 = require("../logger.js");
46
+ const QUOTA_BYTES = 50 * 1024 * 1024; // 50 MB
47
+ async function handlePutBlob(blobId, tenantId, rawBody, isBase64Encoded, contentType, ddb, tableName) {
48
+ // Rate limit check
49
+ const rateResult = await (0, rate_limit_js_1.checkRateLimit)(tenantId, 'PUT', ddb, tableName);
50
+ if (!rateResult.allowed) {
51
+ return {
52
+ statusCode: 429,
53
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
54
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
55
+ };
56
+ }
57
+ if (!rawBody) {
58
+ return {
59
+ statusCode: 400,
60
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
61
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
62
+ };
63
+ }
64
+ const bodyBuffer = isBase64Encoded
65
+ ? Buffer.from(rawBody, 'base64')
66
+ : Buffer.from(rawBody, 'utf-8');
67
+ // Validate input
68
+ const validation = (0, input_validation_js_1.validateBlobUpload)(bodyBuffer, contentType);
69
+ if (!validation.valid) {
70
+ return {
71
+ statusCode: 400,
72
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
73
+ body: JSON.stringify({ error: 'invalid_request', message: validation.error }),
74
+ };
75
+ }
76
+ // Check quota
77
+ const tenantMeta = await ddb.send(new lib_dynamodb_1.GetCommand({
78
+ TableName: tableName,
79
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
80
+ ProjectionExpression: 'storageUsedBytes',
81
+ }));
82
+ const currentUsage = tenantMeta.Item?.['storageUsedBytes'] ?? 0;
83
+ if (currentUsage + bodyBuffer.length > QUOTA_BYTES) {
84
+ return {
85
+ statusCode: 413,
86
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
87
+ body: JSON.stringify({
88
+ error: 'quota_exceeded',
89
+ message: 'Storage quota exceeded (50 MB)',
90
+ currentUsage,
91
+ limit: QUOTA_BYTES,
92
+ }),
93
+ };
94
+ }
95
+ const now = new Date().toISOString();
96
+ const sha256 = crypto.createHash('sha256').update(bodyBuffer).digest('base64');
97
+ // Write-if-absent
98
+ try {
99
+ await ddb.send(new lib_dynamodb_1.PutCommand({
100
+ TableName: tableName,
101
+ Item: {
102
+ PK: `TENANT#${tenantId}`,
103
+ SK: `BLOB#${blobId}`,
104
+ data: bodyBuffer,
105
+ size: bodyBuffer.length,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ },
109
+ ConditionExpression: 'attribute_not_exists(SK)',
110
+ }));
111
+ }
112
+ catch (err) {
113
+ if (err.name === 'ConditionalCheckFailedException') {
114
+ return {
115
+ statusCode: 409,
116
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
117
+ body: JSON.stringify({ error: 'blob_exists', id: blobId }),
118
+ };
119
+ }
120
+ throw err;
121
+ }
122
+ // Atomic increment storage used
123
+ await ddb.send(new lib_dynamodb_1.UpdateCommand({
124
+ TableName: tableName,
125
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
126
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes + :size',
127
+ ExpressionAttributeValues: { ':size': bodyBuffer.length },
128
+ }));
129
+ logger_js_1.logger.info('Blob uploaded', { tenantId, operation: 'PUT', blobCount: 1 });
130
+ return {
131
+ statusCode: 201,
132
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
133
+ body: JSON.stringify({ id: blobId, size: bodyBuffer.length, ts: now, sha256 }),
134
+ };
135
+ }
136
+ async function handleGetBlob(blobId, tenantId, ddb, tableName) {
137
+ const rateResult = await (0, rate_limit_js_1.checkRateLimit)(tenantId, 'GET', ddb, tableName);
138
+ if (!rateResult.allowed) {
139
+ return {
140
+ statusCode: 429,
141
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
142
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
143
+ };
144
+ }
145
+ const result = await ddb.send(new lib_dynamodb_1.GetCommand({
146
+ TableName: tableName,
147
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
148
+ }));
149
+ if (!result.Item || result.Item['deletedAt']) {
150
+ return {
151
+ statusCode: 404,
152
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
153
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
154
+ };
155
+ }
156
+ const data = result.Item['data'];
157
+ return {
158
+ statusCode: 200,
159
+ headers: {
160
+ 'Content-Type': 'application/octet-stream',
161
+ ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult),
162
+ },
163
+ body: Buffer.from(data).toString('base64'),
164
+ };
165
+ }
166
+ async function handleDeleteBlob(blobId, tenantId, ddb, tableName) {
167
+ const rateResult = await (0, rate_limit_js_1.checkRateLimit)(tenantId, 'DELETE', ddb, tableName);
168
+ if (!rateResult.allowed) {
169
+ return {
170
+ statusCode: 429,
171
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
172
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
173
+ };
174
+ }
175
+ const now = new Date().toISOString();
176
+ const ttlEpoch = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days
177
+ // Get blob size for decrementing storage
178
+ const existing = await ddb.send(new lib_dynamodb_1.GetCommand({
179
+ TableName: tableName,
180
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
181
+ ProjectionExpression: '#s',
182
+ ExpressionAttributeNames: { '#s': 'size' },
183
+ }));
184
+ if (!existing.Item) {
185
+ return {
186
+ statusCode: 404,
187
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
188
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
189
+ };
190
+ }
191
+ const blobSize = existing.Item['size'] ?? 0;
192
+ // Soft delete
193
+ await ddb.send(new lib_dynamodb_1.UpdateCommand({
194
+ TableName: tableName,
195
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
196
+ UpdateExpression: 'SET deletedAt = :deletedAt, #ttl = :ttl, updatedAt = :updatedAt',
197
+ ExpressionAttributeNames: { '#ttl': 'ttl' },
198
+ ExpressionAttributeValues: {
199
+ ':deletedAt': now,
200
+ ':ttl': ttlEpoch,
201
+ ':updatedAt': now,
202
+ },
203
+ }));
204
+ // Decrement storage used
205
+ await ddb.send(new lib_dynamodb_1.UpdateCommand({
206
+ TableName: tableName,
207
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
208
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes - :size',
209
+ ExpressionAttributeValues: { ':size': blobSize },
210
+ }));
211
+ logger_js_1.logger.info('Blob soft-deleted', { tenantId, operation: 'DELETE' });
212
+ return {
213
+ statusCode: 200,
214
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
215
+ body: JSON.stringify({ id: blobId, deleted: true }),
216
+ };
217
+ }
218
+ async function handleListBlobs(tenantId, since, ddb, tableName) {
219
+ const rateResult = await (0, rate_limit_js_1.checkRateLimit)(tenantId, 'LIST', ddb, tableName);
220
+ if (!rateResult.allowed) {
221
+ return {
222
+ statusCode: 429,
223
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
224
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
225
+ };
226
+ }
227
+ let items = [];
228
+ if (since) {
229
+ // Use GSI for incremental sync
230
+ const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
231
+ TableName: tableName,
232
+ IndexName: 'updatedAt-index',
233
+ KeyConditionExpression: 'PK = :pk AND updatedAt > :since',
234
+ ExpressionAttributeValues: {
235
+ ':pk': `TENANT#${tenantId}`,
236
+ ':since': since,
237
+ },
238
+ }));
239
+ items = (result.Items ?? []);
240
+ }
241
+ else {
242
+ // Full list — query base table
243
+ const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
244
+ TableName: tableName,
245
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
246
+ ExpressionAttributeValues: {
247
+ ':pk': `TENANT#${tenantId}`,
248
+ ':prefix': 'BLOB#',
249
+ },
250
+ ProjectionExpression: 'SK, #s, updatedAt, deletedAt',
251
+ ExpressionAttributeNames: { '#s': 'size' },
252
+ }));
253
+ items = (result.Items ?? []);
254
+ }
255
+ const blobs = [];
256
+ const tombstones = [];
257
+ for (const item of items) {
258
+ const sk = item['SK'];
259
+ const blobId = sk.startsWith('BLOB#') ? sk.slice(5) : sk;
260
+ if (item['deletedAt']) {
261
+ tombstones.push({
262
+ id: blobId,
263
+ deleted_at: item['deletedAt'],
264
+ });
265
+ }
266
+ else {
267
+ blobs.push({
268
+ id: blobId,
269
+ size: item['size'],
270
+ ts: item['updatedAt'],
271
+ });
272
+ }
273
+ }
274
+ logger_js_1.logger.info('Blobs listed', { tenantId, operation: 'LIST', blobCount: blobs.length });
275
+ return {
276
+ statusCode: 200,
277
+ headers: { 'Content-Type': 'application/json', ...(0, rate_limit_js_1.rateLimitHeaders)(rateResult) },
278
+ body: JSON.stringify({ blobs, tombstones }),
279
+ };
280
+ }
281
+ async function handleCountBlobs(tenantId, ddb, tableName) {
282
+ const result = await ddb.send(new lib_dynamodb_1.QueryCommand({
283
+ TableName: tableName,
284
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
285
+ ExpressionAttributeValues: {
286
+ ':pk': `TENANT#${tenantId}`,
287
+ ':prefix': 'BLOB#',
288
+ },
289
+ FilterExpression: 'attribute_not_exists(deletedAt)',
290
+ Select: 'COUNT',
291
+ }));
292
+ return {
293
+ statusCode: 200,
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({ count: result.Count ?? 0 }),
296
+ };
297
+ }
298
+ //# sourceMappingURL=blobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobs.js","sourceRoot":"","sources":["../../../../lib/handler/routes/blobs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,348 @@
1
+ import * as crypto from 'crypto';
2
+ import {
3
+ DynamoDBDocumentClient,
4
+ PutCommand,
5
+ GetCommand,
6
+ UpdateCommand,
7
+ QueryCommand,
8
+ } from '@aws-sdk/lib-dynamodb';
9
+ import { validateBlobUpload } from '../middleware/input-validation.js';
10
+ import { checkRateLimit, rateLimitHeaders } from '../middleware/rate-limit.js';
11
+ import { logger } from '../logger.js';
12
+
13
+ interface HandlerResponse {
14
+ statusCode: number;
15
+ body: string;
16
+ headers: Record<string, string>;
17
+ }
18
+
19
+ const QUOTA_BYTES = 50 * 1024 * 1024; // 50 MB
20
+
21
+ export async function handlePutBlob(
22
+ blobId: string,
23
+ tenantId: string,
24
+ rawBody: string | null | undefined,
25
+ isBase64Encoded: boolean,
26
+ contentType: string,
27
+ ddb: DynamoDBDocumentClient,
28
+ tableName: string,
29
+ ): Promise<HandlerResponse> {
30
+ // Rate limit check
31
+ const rateResult = await checkRateLimit(tenantId, 'PUT', ddb, tableName);
32
+ if (!rateResult.allowed) {
33
+ return {
34
+ statusCode: 429,
35
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
36
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
37
+ };
38
+ }
39
+
40
+ if (!rawBody) {
41
+ return {
42
+ statusCode: 400,
43
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
44
+ body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
45
+ };
46
+ }
47
+
48
+ const bodyBuffer = isBase64Encoded
49
+ ? Buffer.from(rawBody, 'base64')
50
+ : Buffer.from(rawBody, 'utf-8');
51
+
52
+ // Validate input
53
+ const validation = validateBlobUpload(bodyBuffer, contentType);
54
+ if (!validation.valid) {
55
+ return {
56
+ statusCode: 400,
57
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
58
+ body: JSON.stringify({ error: 'invalid_request', message: validation.error }),
59
+ };
60
+ }
61
+
62
+ // Check quota
63
+ const tenantMeta = await ddb.send(
64
+ new GetCommand({
65
+ TableName: tableName,
66
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
67
+ ProjectionExpression: 'storageUsedBytes',
68
+ }),
69
+ );
70
+
71
+ const currentUsage = (tenantMeta.Item?.['storageUsedBytes'] as number) ?? 0;
72
+ if (currentUsage + bodyBuffer.length > QUOTA_BYTES) {
73
+ return {
74
+ statusCode: 413,
75
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
76
+ body: JSON.stringify({
77
+ error: 'quota_exceeded',
78
+ message: 'Storage quota exceeded (50 MB)',
79
+ currentUsage,
80
+ limit: QUOTA_BYTES,
81
+ }),
82
+ };
83
+ }
84
+
85
+ const now = new Date().toISOString();
86
+ const sha256 = crypto.createHash('sha256').update(bodyBuffer).digest('base64');
87
+
88
+ // Write-if-absent
89
+ try {
90
+ await ddb.send(
91
+ new PutCommand({
92
+ TableName: tableName,
93
+ Item: {
94
+ PK: `TENANT#${tenantId}`,
95
+ SK: `BLOB#${blobId}`,
96
+ data: bodyBuffer,
97
+ size: bodyBuffer.length,
98
+ createdAt: now,
99
+ updatedAt: now,
100
+ },
101
+ ConditionExpression: 'attribute_not_exists(SK)',
102
+ }),
103
+ );
104
+ } catch (err: unknown) {
105
+ if ((err as { name?: string }).name === 'ConditionalCheckFailedException') {
106
+ return {
107
+ statusCode: 409,
108
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
109
+ body: JSON.stringify({ error: 'blob_exists', id: blobId }),
110
+ };
111
+ }
112
+ throw err;
113
+ }
114
+
115
+ // Atomic increment storage used
116
+ await ddb.send(
117
+ new UpdateCommand({
118
+ TableName: tableName,
119
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
120
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes + :size',
121
+ ExpressionAttributeValues: { ':size': bodyBuffer.length },
122
+ }),
123
+ );
124
+
125
+ logger.info('Blob uploaded', { tenantId, operation: 'PUT', blobCount: 1 });
126
+
127
+ return {
128
+ statusCode: 201,
129
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
130
+ body: JSON.stringify({ id: blobId, size: bodyBuffer.length, ts: now, sha256 }),
131
+ };
132
+ }
133
+
134
+ export async function handleGetBlob(
135
+ blobId: string,
136
+ tenantId: string,
137
+ ddb: DynamoDBDocumentClient,
138
+ tableName: string,
139
+ ): Promise<HandlerResponse> {
140
+ const rateResult = await checkRateLimit(tenantId, 'GET', ddb, tableName);
141
+ if (!rateResult.allowed) {
142
+ return {
143
+ statusCode: 429,
144
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
145
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
146
+ };
147
+ }
148
+
149
+ const result = await ddb.send(
150
+ new GetCommand({
151
+ TableName: tableName,
152
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
153
+ }),
154
+ );
155
+
156
+ if (!result.Item || result.Item['deletedAt']) {
157
+ return {
158
+ statusCode: 404,
159
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
160
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
161
+ };
162
+ }
163
+
164
+ const data = result.Item['data'] as Buffer;
165
+ return {
166
+ statusCode: 200,
167
+ headers: {
168
+ 'Content-Type': 'application/octet-stream',
169
+ ...rateLimitHeaders(rateResult),
170
+ },
171
+ body: Buffer.from(data).toString('base64'),
172
+ };
173
+ }
174
+
175
+ export async function handleDeleteBlob(
176
+ blobId: string,
177
+ tenantId: string,
178
+ ddb: DynamoDBDocumentClient,
179
+ tableName: string,
180
+ ): Promise<HandlerResponse> {
181
+ const rateResult = await checkRateLimit(tenantId, 'DELETE', ddb, tableName);
182
+ if (!rateResult.allowed) {
183
+ return {
184
+ statusCode: 429,
185
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
186
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
187
+ };
188
+ }
189
+
190
+ const now = new Date().toISOString();
191
+ const ttlEpoch = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days
192
+
193
+ // Get blob size for decrementing storage
194
+ const existing = await ddb.send(
195
+ new GetCommand({
196
+ TableName: tableName,
197
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
198
+ ProjectionExpression: '#s',
199
+ ExpressionAttributeNames: { '#s': 'size' },
200
+ }),
201
+ );
202
+
203
+ if (!existing.Item) {
204
+ return {
205
+ statusCode: 404,
206
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
207
+ body: JSON.stringify({ error: 'not_found', message: 'Blob not found' }),
208
+ };
209
+ }
210
+
211
+ const blobSize = (existing.Item['size'] as number) ?? 0;
212
+
213
+ // Soft delete
214
+ await ddb.send(
215
+ new UpdateCommand({
216
+ TableName: tableName,
217
+ Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
218
+ UpdateExpression: 'SET deletedAt = :deletedAt, #ttl = :ttl, updatedAt = :updatedAt',
219
+ ExpressionAttributeNames: { '#ttl': 'ttl' },
220
+ ExpressionAttributeValues: {
221
+ ':deletedAt': now,
222
+ ':ttl': ttlEpoch,
223
+ ':updatedAt': now,
224
+ },
225
+ }),
226
+ );
227
+
228
+ // Decrement storage used
229
+ await ddb.send(
230
+ new UpdateCommand({
231
+ TableName: tableName,
232
+ Key: { PK: `TENANT#${tenantId}`, SK: 'META' },
233
+ UpdateExpression: 'SET storageUsedBytes = storageUsedBytes - :size',
234
+ ExpressionAttributeValues: { ':size': blobSize },
235
+ }),
236
+ );
237
+
238
+ logger.info('Blob soft-deleted', { tenantId, operation: 'DELETE' });
239
+
240
+ return {
241
+ statusCode: 200,
242
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
243
+ body: JSON.stringify({ id: blobId, deleted: true }),
244
+ };
245
+ }
246
+
247
+ export async function handleListBlobs(
248
+ tenantId: string,
249
+ since: string | undefined,
250
+ ddb: DynamoDBDocumentClient,
251
+ tableName: string,
252
+ ): Promise<HandlerResponse> {
253
+ const rateResult = await checkRateLimit(tenantId, 'LIST', ddb, tableName);
254
+ if (!rateResult.allowed) {
255
+ return {
256
+ statusCode: 429,
257
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
258
+ body: JSON.stringify({ error: 'rate_limited', message: 'Too many requests' }),
259
+ };
260
+ }
261
+
262
+ let items: Record<string, unknown>[] = [];
263
+
264
+ if (since) {
265
+ // Use GSI for incremental sync
266
+ const result = await ddb.send(
267
+ new QueryCommand({
268
+ TableName: tableName,
269
+ IndexName: 'updatedAt-index',
270
+ KeyConditionExpression: 'PK = :pk AND updatedAt > :since',
271
+ ExpressionAttributeValues: {
272
+ ':pk': `TENANT#${tenantId}`,
273
+ ':since': since,
274
+ },
275
+ }),
276
+ );
277
+ items = (result.Items ?? []) as Record<string, unknown>[];
278
+ } else {
279
+ // Full list — query base table
280
+ const result = await ddb.send(
281
+ new QueryCommand({
282
+ TableName: tableName,
283
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
284
+ ExpressionAttributeValues: {
285
+ ':pk': `TENANT#${tenantId}`,
286
+ ':prefix': 'BLOB#',
287
+ },
288
+ ProjectionExpression: 'SK, #s, updatedAt, deletedAt',
289
+ ExpressionAttributeNames: { '#s': 'size' },
290
+ }),
291
+ );
292
+ items = (result.Items ?? []) as Record<string, unknown>[];
293
+ }
294
+
295
+ const blobs: { id: string; size: number; ts: string }[] = [];
296
+ const tombstones: { id: string; deleted_at: string }[] = [];
297
+
298
+ for (const item of items) {
299
+ const sk = item['SK'] as string;
300
+ const blobId = sk.startsWith('BLOB#') ? sk.slice(5) : sk;
301
+
302
+ if (item['deletedAt']) {
303
+ tombstones.push({
304
+ id: blobId,
305
+ deleted_at: item['deletedAt'] as string,
306
+ });
307
+ } else {
308
+ blobs.push({
309
+ id: blobId,
310
+ size: item['size'] as number,
311
+ ts: item['updatedAt'] as string,
312
+ });
313
+ }
314
+ }
315
+
316
+ logger.info('Blobs listed', { tenantId, operation: 'LIST', blobCount: blobs.length });
317
+
318
+ return {
319
+ statusCode: 200,
320
+ headers: { 'Content-Type': 'application/json', ...rateLimitHeaders(rateResult) },
321
+ body: JSON.stringify({ blobs, tombstones }),
322
+ };
323
+ }
324
+
325
+ export async function handleCountBlobs(
326
+ tenantId: string,
327
+ ddb: DynamoDBDocumentClient,
328
+ tableName: string,
329
+ ): Promise<HandlerResponse> {
330
+ const result = await ddb.send(
331
+ new QueryCommand({
332
+ TableName: tableName,
333
+ KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
334
+ ExpressionAttributeValues: {
335
+ ':pk': `TENANT#${tenantId}`,
336
+ ':prefix': 'BLOB#',
337
+ },
338
+ FilterExpression: 'attribute_not_exists(deletedAt)',
339
+ Select: 'COUNT',
340
+ }),
341
+ );
342
+
343
+ return {
344
+ statusCode: 200,
345
+ headers: { 'Content-Type': 'application/json' },
346
+ body: JSON.stringify({ count: result.Count ?? 0 }),
347
+ };
348
+ }