@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.
- package/dist/lib/admin-handler/index.d.ts +22 -0
- package/dist/lib/admin-handler/index.d.ts.map +1 -0
- package/dist/lib/admin-handler/index.js +92 -0
- package/dist/lib/admin-handler/index.js.map +1 -0
- package/dist/lib/admin-handler/index.ts +123 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts +11 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.js +200 -0
- package/dist/lib/admin-handler/routes/metrics.js.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.ts +234 -0
- package/dist/lib/admin-handler/routes/overview.d.ts +9 -0
- package/dist/lib/admin-handler/routes/overview.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/overview.js +110 -0
- package/dist/lib/admin-handler/routes/overview.js.map +1 -0
- package/dist/lib/admin-handler/routes/overview.ts +133 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts +10 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.js +108 -0
- package/dist/lib/admin-handler/routes/tenants.js.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.ts +134 -0
- package/dist/lib/chaoskb-stack.d.ts +22 -0
- package/dist/lib/chaoskb-stack.d.ts.map +1 -0
- package/dist/lib/chaoskb-stack.js +60 -0
- package/dist/lib/chaoskb-stack.js.map +1 -0
- package/dist/lib/constructs/admin-api.d.ts +16 -0
- package/dist/lib/constructs/admin-api.d.ts.map +1 -0
- package/dist/lib/constructs/admin-api.js +93 -0
- package/dist/lib/constructs/admin-api.js.map +1 -0
- package/dist/lib/constructs/admin-dashboard.d.ts +18 -0
- package/dist/lib/constructs/admin-dashboard.d.ts.map +1 -0
- package/dist/lib/constructs/admin-dashboard.js +172 -0
- package/dist/lib/constructs/admin-dashboard.js.map +1 -0
- package/dist/lib/constructs/api.d.ts +17 -0
- package/dist/lib/constructs/api.d.ts.map +1 -0
- package/dist/lib/constructs/api.js +81 -0
- package/dist/lib/constructs/api.js.map +1 -0
- package/dist/lib/constructs/auth.d.ts +11 -0
- package/dist/lib/constructs/auth.d.ts.map +1 -0
- package/dist/lib/constructs/auth.js +18 -0
- package/dist/lib/constructs/auth.js.map +1 -0
- package/dist/lib/constructs/blob-store.d.ts +10 -0
- package/dist/lib/constructs/blob-store.d.ts.map +1 -0
- package/dist/lib/constructs/blob-store.js +31 -0
- package/dist/lib/constructs/blob-store.js.map +1 -0
- package/dist/lib/deploy-cli.d.ts +3 -0
- package/dist/lib/deploy-cli.d.ts.map +1 -0
- package/dist/lib/deploy-cli.js +49 -0
- package/dist/lib/deploy-cli.js.map +1 -0
- package/dist/lib/handler/index.d.ts +23 -0
- package/dist/lib/handler/index.d.ts.map +1 -0
- package/dist/lib/handler/index.js +276 -0
- package/dist/lib/handler/index.js.map +1 -0
- package/dist/lib/handler/index.ts +372 -0
- package/dist/lib/handler/logger.d.ts +16 -0
- package/dist/lib/handler/logger.d.ts.map +1 -0
- package/dist/lib/handler/logger.js +26 -0
- package/dist/lib/handler/logger.js.map +1 -0
- package/dist/lib/handler/logger.ts +36 -0
- package/dist/lib/handler/middleware/input-validation.d.ts +6 -0
- package/dist/lib/handler/middleware/input-validation.d.ts.map +1 -0
- package/dist/lib/handler/middleware/input-validation.js +36 -0
- package/dist/lib/handler/middleware/input-validation.js.map +1 -0
- package/dist/lib/handler/middleware/input-validation.ts +44 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts +14 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.js +94 -0
- package/dist/lib/handler/middleware/rate-limit.js.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.ts +121 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts +48 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.js +256 -0
- package/dist/lib/handler/middleware/ssh-auth.js.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.ts +300 -0
- package/dist/lib/handler/routes/audit.d.ts +24 -0
- package/dist/lib/handler/routes/audit.d.ts.map +1 -0
- package/dist/lib/handler/routes/audit.js +94 -0
- package/dist/lib/handler/routes/audit.js.map +1 -0
- package/dist/lib/handler/routes/audit.ts +101 -0
- package/dist/lib/handler/routes/blobs.d.ts +13 -0
- package/dist/lib/handler/routes/blobs.d.ts.map +1 -0
- package/dist/lib/handler/routes/blobs.js +298 -0
- package/dist/lib/handler/routes/blobs.js.map +1 -0
- package/dist/lib/handler/routes/blobs.ts +348 -0
- package/dist/lib/handler/routes/devices.d.ts +48 -0
- package/dist/lib/handler/routes/devices.d.ts.map +1 -0
- package/dist/lib/handler/routes/devices.js +394 -0
- package/dist/lib/handler/routes/devices.js.map +1 -0
- package/dist/lib/handler/routes/devices.ts +458 -0
- package/dist/lib/handler/routes/export.d.ts +9 -0
- package/dist/lib/handler/routes/export.d.ts.map +1 -0
- package/dist/lib/handler/routes/export.js +40 -0
- package/dist/lib/handler/routes/export.js.map +1 -0
- package/dist/lib/handler/routes/export.ts +55 -0
- package/dist/lib/handler/routes/github.d.ts +31 -0
- package/dist/lib/handler/routes/github.d.ts.map +1 -0
- package/dist/lib/handler/routes/github.js +118 -0
- package/dist/lib/handler/routes/github.js.map +1 -0
- package/dist/lib/handler/routes/github.ts +162 -0
- package/dist/lib/handler/routes/health.d.ts +6 -0
- package/dist/lib/handler/routes/health.d.ts.map +1 -0
- package/dist/lib/handler/routes/health.js +14 -0
- package/dist/lib/handler/routes/health.js.map +1 -0
- package/dist/lib/handler/routes/health.ts +10 -0
- package/dist/lib/handler/routes/invites.d.ts +24 -0
- package/dist/lib/handler/routes/invites.d.ts.map +1 -0
- package/dist/lib/handler/routes/invites.js +445 -0
- package/dist/lib/handler/routes/invites.js.map +1 -0
- package/dist/lib/handler/routes/invites.ts +527 -0
- package/dist/lib/handler/routes/notifications.d.ts +39 -0
- package/dist/lib/handler/routes/notifications.d.ts.map +1 -0
- package/dist/lib/handler/routes/notifications.js +150 -0
- package/dist/lib/handler/routes/notifications.js.map +1 -0
- package/dist/lib/handler/routes/notifications.ts +163 -0
- package/dist/lib/handler/routes/projects.d.ts +24 -0
- package/dist/lib/handler/routes/projects.d.ts.map +1 -0
- package/dist/lib/handler/routes/projects.js +47 -0
- package/dist/lib/handler/routes/projects.js.map +1 -0
- package/dist/lib/handler/routes/projects.ts +69 -0
- package/dist/lib/handler/routes/register.d.ts +19 -0
- package/dist/lib/handler/routes/register.d.ts.map +1 -0
- package/dist/lib/handler/routes/register.js +327 -0
- package/dist/lib/handler/routes/register.js.map +1 -0
- package/dist/lib/handler/routes/register.ts +363 -0
- package/dist/lib/handler/routes/restore.d.ts +9 -0
- package/dist/lib/handler/routes/restore.d.ts.map +1 -0
- package/dist/lib/handler/routes/restore.js +52 -0
- package/dist/lib/handler/routes/restore.js.map +1 -0
- package/dist/lib/handler/routes/restore.ts +73 -0
- package/dist/lib/handler/routes/revocation.d.ts +13 -0
- package/dist/lib/handler/routes/revocation.d.ts.map +1 -0
- package/dist/lib/handler/routes/revocation.js +63 -0
- package/dist/lib/handler/routes/revocation.js.map +1 -0
- package/dist/lib/handler/routes/revocation.ts +87 -0
- package/dist/lib/handler/routes/rotation.d.ts +24 -0
- package/dist/lib/handler/routes/rotation.d.ts.map +1 -0
- package/dist/lib/handler/routes/rotation.js +291 -0
- package/dist/lib/handler/routes/rotation.js.map +1 -0
- package/dist/lib/handler/routes/rotation.ts +336 -0
- package/dist/lib/handler/routes/tenants.d.ts +11 -0
- package/dist/lib/handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/handler/routes/tenants.js +181 -0
- package/dist/lib/handler/routes/tenants.js.map +1 -0
- package/dist/lib/handler/routes/tenants.ts +198 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts +21 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.js +76 -0
- package/dist/lib/handler/routes/wrapped-key.js.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.ts +108 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +16 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +18 -0
- package/dist/vitest.config.js.map +1 -0
- 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"}
|