@de-otio/chaoskb-server 0.2.0 → 0.2.2
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/constructs/blob-store.js +1 -1
- package/dist/lib/constructs/blob-store.js.map +1 -1
- package/dist/lib/handler/index.d.ts.map +1 -1
- package/dist/lib/handler/index.js +20 -4
- package/dist/lib/handler/index.js.map +1 -1
- package/dist/lib/handler/index.ts +19 -4
- package/dist/lib/handler/middleware/rate-limit.d.ts.map +1 -1
- package/dist/lib/handler/middleware/rate-limit.js +13 -9
- package/dist/lib/handler/middleware/rate-limit.js.map +1 -1
- package/dist/lib/handler/middleware/rate-limit.ts +14 -9
- package/dist/lib/handler/middleware/ssh-auth.d.ts.map +1 -1
- package/dist/lib/handler/middleware/ssh-auth.js +66 -6
- package/dist/lib/handler/middleware/ssh-auth.js.map +1 -1
- package/dist/lib/handler/middleware/ssh-auth.ts +74 -7
- package/dist/lib/handler/routes/audit.js +1 -1
- package/dist/lib/handler/routes/audit.js.map +1 -1
- package/dist/lib/handler/routes/audit.ts +1 -1
- package/dist/lib/handler/routes/blobs.d.ts.map +1 -1
- package/dist/lib/handler/routes/blobs.js +2 -3
- package/dist/lib/handler/routes/blobs.js.map +1 -1
- package/dist/lib/handler/routes/blobs.ts +2 -3
- package/dist/lib/handler/routes/devices.d.ts.map +1 -1
- package/dist/lib/handler/routes/devices.js +19 -12
- package/dist/lib/handler/routes/devices.js.map +1 -1
- package/dist/lib/handler/routes/devices.ts +20 -12
- package/dist/lib/handler/routes/github.d.ts +15 -2
- package/dist/lib/handler/routes/github.d.ts.map +1 -1
- package/dist/lib/handler/routes/github.js +96 -22
- package/dist/lib/handler/routes/github.js.map +1 -1
- package/dist/lib/handler/routes/github.ts +68 -35
- package/dist/lib/handler/routes/invites.d.ts.map +1 -1
- package/dist/lib/handler/routes/invites.js +11 -13
- package/dist/lib/handler/routes/invites.js.map +1 -1
- package/dist/lib/handler/routes/invites.ts +11 -13
- package/dist/lib/handler/routes/notifications.js +1 -1
- package/dist/lib/handler/routes/notifications.js.map +1 -1
- package/dist/lib/handler/routes/notifications.ts +1 -1
- package/dist/lib/handler/routes/projects.d.ts.map +1 -1
- package/dist/lib/handler/routes/projects.js.map +1 -1
- package/dist/lib/handler/routes/projects.ts +0 -1
- package/dist/lib/handler/routes/register.d.ts +1 -1
- package/dist/lib/handler/routes/register.d.ts.map +1 -1
- package/dist/lib/handler/routes/register.js +104 -58
- package/dist/lib/handler/routes/register.js.map +1 -1
- package/dist/lib/handler/routes/register.ts +113 -66
- package/dist/lib/handler/routes/restore.d.ts.map +1 -1
- package/dist/lib/handler/routes/restore.js +1 -2
- package/dist/lib/handler/routes/restore.js.map +1 -1
- package/dist/lib/handler/routes/restore.ts +1 -2
- package/dist/lib/handler/routes/rotation.d.ts.map +1 -1
- package/dist/lib/handler/routes/rotation.js +23 -2
- package/dist/lib/handler/routes/rotation.js.map +1 -1
- package/dist/lib/handler/routes/rotation.ts +30 -2
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ const audit_js_1 = require("./audit.js");
|
|
|
43
43
|
const client_ssm_1 = require("@aws-sdk/client-ssm");
|
|
44
44
|
const logger_js_1 = require("../logger.js");
|
|
45
45
|
const github_js_1 = require("./github.js");
|
|
46
|
+
const notifications_js_1 = require("./notifications.js");
|
|
46
47
|
const CHALLENGE_EXPIRY_SECONDS = 60;
|
|
47
48
|
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
48
49
|
let cachedSignupsEnabled = null;
|
|
@@ -121,24 +122,24 @@ async function handleChallenge(ddb, tableName) {
|
|
|
121
122
|
const nonce = crypto.randomBytes(32).toString('base64');
|
|
122
123
|
const now = Math.floor(Date.now() / 1000);
|
|
123
124
|
const ttl = now + CHALLENGE_EXPIRY_SECONDS + 60; // DynamoDB TTL: generous buffer
|
|
124
|
-
const
|
|
125
|
+
const expiresAtISO = new Date((now + CHALLENGE_EXPIRY_SECONDS) * 1000).toISOString();
|
|
125
126
|
await ddb.send(new lib_dynamodb_1.PutCommand({
|
|
126
127
|
TableName: tableName,
|
|
127
128
|
Item: {
|
|
128
129
|
PK: `CHALLENGE#${nonce}`,
|
|
129
130
|
SK: 'META',
|
|
130
|
-
|
|
131
|
-
ttl,
|
|
131
|
+
expiresAtISO,
|
|
132
|
+
expiresAt: ttl,
|
|
132
133
|
},
|
|
133
134
|
}));
|
|
134
135
|
logger_js_1.logger.info('Registration challenge created');
|
|
135
136
|
return {
|
|
136
137
|
statusCode: 200,
|
|
137
138
|
headers: JSON_HEADERS,
|
|
138
|
-
body: JSON.stringify({ challenge: nonce, expiresAt }),
|
|
139
|
+
body: JSON.stringify({ challenge: nonce, expiresAt: expiresAtISO }),
|
|
139
140
|
};
|
|
140
141
|
}
|
|
141
|
-
async function handleRegister(body, ddb, tableName, signupsParamName) {
|
|
142
|
+
async function handleRegister(body, ddb, tableName, signupsParamName, headers = {}) {
|
|
142
143
|
// Check if signups are enabled
|
|
143
144
|
const signupsEnabled = await checkSignupsEnabled(signupsParamName);
|
|
144
145
|
if (!signupsEnabled) {
|
|
@@ -188,39 +189,39 @@ async function handleRegister(body, ddb, tableName, signupsParamName) {
|
|
|
188
189
|
body: JSON.stringify({ error: 'invalid_request', message: 'Invalid SSH public key format' }),
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
SK: 'META',
|
|
197
|
-
},
|
|
198
|
-
}));
|
|
199
|
-
if (!challengeResult.Item) {
|
|
200
|
-
return {
|
|
201
|
-
statusCode: 400,
|
|
202
|
-
headers: JSON_HEADERS,
|
|
203
|
-
body: JSON.stringify({ error: 'invalid_challenge', message: 'Challenge not found or already used' }),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Check challenge expiry
|
|
207
|
-
if (new Date(challengeResult.Item['expiresAt']) < new Date()) {
|
|
208
|
-
// Clean up expired challenge
|
|
209
|
-
await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
192
|
+
// Atomically consume the challenge nonce (single-use).
|
|
193
|
+
// Uses conditional delete to prevent TOCTOU race: only one request can consume a given nonce.
|
|
194
|
+
let challengeItem;
|
|
195
|
+
try {
|
|
196
|
+
const deleteResult = await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
210
197
|
TableName: tableName,
|
|
211
|
-
Key: {
|
|
198
|
+
Key: {
|
|
199
|
+
PK: `CHALLENGE#${request.challengeNonce}`,
|
|
200
|
+
SK: 'META',
|
|
201
|
+
},
|
|
202
|
+
ConditionExpression: 'attribute_exists(PK)',
|
|
203
|
+
ReturnValues: 'ALL_OLD',
|
|
212
204
|
}));
|
|
205
|
+
challengeItem = deleteResult.Attributes;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err.name === 'ConditionalCheckFailedException') {
|
|
209
|
+
return {
|
|
210
|
+
statusCode: 400,
|
|
211
|
+
headers: JSON_HEADERS,
|
|
212
|
+
body: JSON.stringify({ error: 'invalid_challenge', message: 'Challenge not found or already used' }),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
// Check challenge expiry on the consumed item
|
|
218
|
+
if (!challengeItem || new Date(challengeItem['expiresAtISO']) < new Date()) {
|
|
213
219
|
return {
|
|
214
220
|
statusCode: 400,
|
|
215
221
|
headers: JSON_HEADERS,
|
|
216
222
|
body: JSON.stringify({ error: 'challenge_expired', message: 'Challenge has expired' }),
|
|
217
223
|
};
|
|
218
224
|
}
|
|
219
|
-
// Consume the challenge (delete it — single-use)
|
|
220
|
-
await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
221
|
-
TableName: tableName,
|
|
222
|
-
Key: { PK: `CHALLENGE#${request.challengeNonce}`, SK: 'META' },
|
|
223
|
-
}));
|
|
224
225
|
// Verify the SSH signature of the challenge nonce against the public key
|
|
225
226
|
const validSignature = verifyRegistrationSignature(request.publicKey, request.challengeNonce, request.signedChallenge);
|
|
226
227
|
if (!validSignature) {
|
|
@@ -233,45 +234,79 @@ async function handleRegister(body, ddb, tableName, signupsParamName) {
|
|
|
233
234
|
}
|
|
234
235
|
// GitHub verification (if --github was provided)
|
|
235
236
|
if (request.github) {
|
|
237
|
+
let keyVerified = false;
|
|
236
238
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
keyVerified = await (0, github_js_1.verifyKeyOnGitHub)(request.publicKey, request.github);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// GitHub unreachable or user not found — uniform response
|
|
243
|
+
}
|
|
244
|
+
if (!keyVerified) {
|
|
245
|
+
return {
|
|
246
|
+
statusCode: 400,
|
|
247
|
+
headers: JSON_HEADERS,
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
error: 'github_verification_failed',
|
|
250
|
+
message: 'Could not verify key against this GitHub account',
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Check if an existing tenant is associated with this GitHub username (auto-link)
|
|
255
|
+
const existingTenantId = await (0, github_js_1.findTenantByGitHub)(request.github, ddb, tableName);
|
|
256
|
+
if (existingTenantId) {
|
|
257
|
+
// Fresh-fetch GitHub keys (bypass cache) to ensure both device keys still appear
|
|
258
|
+
let freshKeys;
|
|
259
|
+
try {
|
|
260
|
+
freshKeys = await (0, github_js_1.fetchGitHubKeysFresh)(request.github);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// GitHub unreachable at auto-link time — fall back to normal registration
|
|
264
|
+
freshKeys = [];
|
|
265
|
+
}
|
|
266
|
+
// Verify the new device's key appears on the GitHub account (fresh data)
|
|
267
|
+
if (freshKeys.length > 0 && !(0, github_js_1.keyAppearsInGitHubKeys)(request.publicKey, freshKeys)) {
|
|
239
268
|
return {
|
|
240
269
|
statusCode: 400,
|
|
241
270
|
headers: JSON_HEADERS,
|
|
242
271
|
body: JSON.stringify({
|
|
243
|
-
error: '
|
|
244
|
-
message:
|
|
272
|
+
error: 'github_verification_failed',
|
|
273
|
+
message: 'Could not verify key against this GitHub account',
|
|
245
274
|
}),
|
|
246
275
|
};
|
|
247
276
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
277
|
+
if (freshKeys.length > 0) {
|
|
278
|
+
// Resolve location from CloudFront headers for the notification
|
|
279
|
+
const location = (0, notifications_js_1.resolveIpLocation)(headers);
|
|
280
|
+
const deviceInfo = {
|
|
281
|
+
...request.deviceInfo,
|
|
282
|
+
location,
|
|
283
|
+
};
|
|
284
|
+
// Create notification for existing devices
|
|
285
|
+
await (0, notifications_js_1.createNotification)(existingTenantId, 'device_linked', deviceInfo, ddb, tableName);
|
|
286
|
+
// Audit event
|
|
287
|
+
await (0, audit_js_1.logAuditEvent)(ddb, tableName, existingTenantId, {
|
|
288
|
+
eventType: 'device-linked',
|
|
289
|
+
fingerprint: '',
|
|
290
|
+
metadata: {
|
|
291
|
+
publicKey: request.publicKey,
|
|
292
|
+
github: request.github,
|
|
293
|
+
...(deviceInfo.hostname && { hostname: deviceInfo.hostname }),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
logger_js_1.logger.info('GitHub auto-link: existing tenant found', {
|
|
297
|
+
existingTenantId,
|
|
298
|
+
github: request.github,
|
|
299
|
+
});
|
|
251
300
|
return {
|
|
252
|
-
statusCode:
|
|
301
|
+
statusCode: 200,
|
|
253
302
|
headers: JSON_HEADERS,
|
|
254
|
-
body: JSON.stringify({
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
status: 'auto_linked',
|
|
305
|
+
github: request.github,
|
|
306
|
+
}),
|
|
255
307
|
};
|
|
256
308
|
}
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
// Check if an existing tenant is associated with this GitHub username (auto-link)
|
|
260
|
-
const existingTenantId = await (0, github_js_1.findTenantByGitHub)(request.github, ddb, tableName);
|
|
261
|
-
if (existingTenantId) {
|
|
262
|
-
logger_js_1.logger.info('GitHub auto-link: existing tenant found', {
|
|
263
|
-
existingTenantId,
|
|
264
|
-
github: request.github,
|
|
265
|
-
});
|
|
266
|
-
return {
|
|
267
|
-
statusCode: 200,
|
|
268
|
-
headers: JSON_HEADERS,
|
|
269
|
-
body: JSON.stringify({
|
|
270
|
-
status: 'auto_linked',
|
|
271
|
-
tenantId: existingTenantId,
|
|
272
|
-
github: request.github,
|
|
273
|
-
}),
|
|
274
|
-
};
|
|
309
|
+
// If fresh keys unavailable, fall through to normal registration
|
|
275
310
|
}
|
|
276
311
|
}
|
|
277
312
|
const tenantId = tenantIdFromPublicKey(request.publicKey);
|
|
@@ -292,8 +327,19 @@ async function handleRegister(body, ddb, tableName, signupsParamName) {
|
|
|
292
327
|
logger_js_1.logger.info('Tenant registered', { tenantId, operation: 'register' });
|
|
293
328
|
// Store GitHub association if provided
|
|
294
329
|
if (request.github) {
|
|
330
|
+
const claimed = await (0, github_js_1.storeGitHubReverseLookup)(request.github, tenantId, ddb, tableName);
|
|
331
|
+
if (!claimed) {
|
|
332
|
+
// Another tenant already claimed this GitHub username — uniform error
|
|
333
|
+
return {
|
|
334
|
+
statusCode: 400,
|
|
335
|
+
headers: JSON_HEADERS,
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
error: 'github_verification_failed',
|
|
338
|
+
message: 'Could not verify key against this GitHub account',
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
295
342
|
await (0, github_js_1.storeGitHubAssociation)(tenantId, request.github, ddb, tableName);
|
|
296
|
-
await (0, github_js_1.storeGitHubReverseLookup)(request.github, tenantId, ddb, tableName);
|
|
297
343
|
}
|
|
298
344
|
await (0, audit_js_1.logAuditEvent)(ddb, tableName, tenantId, {
|
|
299
345
|
eventType: 'registered',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../../lib/handler/routes/register.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../../lib/handler/routes/register.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
|
-
import { DynamoDBDocumentClient, PutCommand,
|
|
2
|
+
import { DynamoDBDocumentClient, PutCommand, DeleteCommand } from '@aws-sdk/lib-dynamodb';
|
|
3
3
|
import { logAuditEvent } from './audit.js';
|
|
4
4
|
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
5
5
|
import { logger } from '../logger.js';
|
|
6
6
|
import {
|
|
7
7
|
verifyKeyOnGitHub,
|
|
8
|
+
fetchGitHubKeysFresh,
|
|
9
|
+
keyAppearsInGitHubKeys,
|
|
8
10
|
storeGitHubAssociation,
|
|
9
11
|
storeGitHubReverseLookup,
|
|
10
12
|
findTenantByGitHub,
|
|
11
|
-
GitHubVerificationError,
|
|
12
13
|
} from './github.js';
|
|
14
|
+
import { createNotification, resolveIpLocation, type DeviceInfo } from './notifications.js';
|
|
13
15
|
|
|
14
16
|
interface RegisterRequest {
|
|
15
17
|
publicKey: string;
|
|
16
18
|
signedChallenge: string;
|
|
17
19
|
challengeNonce: string;
|
|
18
20
|
github?: string;
|
|
21
|
+
deviceInfo?: DeviceInfo;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
interface HandlerResponse {
|
|
@@ -119,7 +122,7 @@ export async function handleChallenge(
|
|
|
119
122
|
const nonce = crypto.randomBytes(32).toString('base64');
|
|
120
123
|
const now = Math.floor(Date.now() / 1000);
|
|
121
124
|
const ttl = now + CHALLENGE_EXPIRY_SECONDS + 60; // DynamoDB TTL: generous buffer
|
|
122
|
-
const
|
|
125
|
+
const expiresAtISO = new Date((now + CHALLENGE_EXPIRY_SECONDS) * 1000).toISOString();
|
|
123
126
|
|
|
124
127
|
await ddb.send(
|
|
125
128
|
new PutCommand({
|
|
@@ -127,8 +130,8 @@ export async function handleChallenge(
|
|
|
127
130
|
Item: {
|
|
128
131
|
PK: `CHALLENGE#${nonce}`,
|
|
129
132
|
SK: 'META',
|
|
130
|
-
|
|
131
|
-
ttl,
|
|
133
|
+
expiresAtISO,
|
|
134
|
+
expiresAt: ttl,
|
|
132
135
|
},
|
|
133
136
|
}),
|
|
134
137
|
);
|
|
@@ -138,7 +141,7 @@ export async function handleChallenge(
|
|
|
138
141
|
return {
|
|
139
142
|
statusCode: 200,
|
|
140
143
|
headers: JSON_HEADERS,
|
|
141
|
-
body: JSON.stringify({ challenge: nonce, expiresAt }),
|
|
144
|
+
body: JSON.stringify({ challenge: nonce, expiresAt: expiresAtISO }),
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -147,6 +150,7 @@ export async function handleRegister(
|
|
|
147
150
|
ddb: DynamoDBDocumentClient,
|
|
148
151
|
tableName: string,
|
|
149
152
|
signupsParamName: string,
|
|
153
|
+
headers: Record<string, string> = {},
|
|
150
154
|
): Promise<HandlerResponse> {
|
|
151
155
|
// Check if signups are enabled
|
|
152
156
|
const signupsEnabled = await checkSignupsEnabled(signupsParamName);
|
|
@@ -202,34 +206,35 @@ export async function handleRegister(
|
|
|
202
206
|
};
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
PK: `CHALLENGE#${request.challengeNonce}`,
|
|
211
|
-
SK: 'META',
|
|
212
|
-
},
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
if (!challengeResult.Item) {
|
|
217
|
-
return {
|
|
218
|
-
statusCode: 400,
|
|
219
|
-
headers: JSON_HEADERS,
|
|
220
|
-
body: JSON.stringify({ error: 'invalid_challenge', message: 'Challenge not found or already used' }),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Check challenge expiry
|
|
225
|
-
if (new Date(challengeResult.Item['expiresAt'] as string) < new Date()) {
|
|
226
|
-
// Clean up expired challenge
|
|
227
|
-
await ddb.send(
|
|
209
|
+
// Atomically consume the challenge nonce (single-use).
|
|
210
|
+
// Uses conditional delete to prevent TOCTOU race: only one request can consume a given nonce.
|
|
211
|
+
let challengeItem: Record<string, unknown> | undefined;
|
|
212
|
+
try {
|
|
213
|
+
const deleteResult = await ddb.send(
|
|
228
214
|
new DeleteCommand({
|
|
229
215
|
TableName: tableName,
|
|
230
|
-
Key: {
|
|
216
|
+
Key: {
|
|
217
|
+
PK: `CHALLENGE#${request.challengeNonce}`,
|
|
218
|
+
SK: 'META',
|
|
219
|
+
},
|
|
220
|
+
ConditionExpression: 'attribute_exists(PK)',
|
|
221
|
+
ReturnValues: 'ALL_OLD',
|
|
231
222
|
}),
|
|
232
223
|
);
|
|
224
|
+
challengeItem = deleteResult.Attributes;
|
|
225
|
+
} catch (err: unknown) {
|
|
226
|
+
if ((err as { name?: string }).name === 'ConditionalCheckFailedException') {
|
|
227
|
+
return {
|
|
228
|
+
statusCode: 400,
|
|
229
|
+
headers: JSON_HEADERS,
|
|
230
|
+
body: JSON.stringify({ error: 'invalid_challenge', message: 'Challenge not found or already used' }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check challenge expiry on the consumed item
|
|
237
|
+
if (!challengeItem || new Date(challengeItem['expiresAtISO'] as string) < new Date()) {
|
|
233
238
|
return {
|
|
234
239
|
statusCode: 400,
|
|
235
240
|
headers: JSON_HEADERS,
|
|
@@ -237,14 +242,6 @@ export async function handleRegister(
|
|
|
237
242
|
};
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
// Consume the challenge (delete it — single-use)
|
|
241
|
-
await ddb.send(
|
|
242
|
-
new DeleteCommand({
|
|
243
|
-
TableName: tableName,
|
|
244
|
-
Key: { PK: `CHALLENGE#${request.challengeNonce}`, SK: 'META' },
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
245
|
// Verify the SSH signature of the challenge nonce against the public key
|
|
249
246
|
const validSignature = verifyRegistrationSignature(
|
|
250
247
|
request.publicKey,
|
|
@@ -263,45 +260,84 @@ export async function handleRegister(
|
|
|
263
260
|
|
|
264
261
|
// GitHub verification (if --github was provided)
|
|
265
262
|
if (request.github) {
|
|
263
|
+
let keyVerified = false;
|
|
266
264
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
keyVerified = await verifyKeyOnGitHub(request.publicKey, request.github);
|
|
266
|
+
} catch {
|
|
267
|
+
// GitHub unreachable or user not found — uniform response
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!keyVerified) {
|
|
271
|
+
return {
|
|
272
|
+
statusCode: 400,
|
|
273
|
+
headers: JSON_HEADERS,
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
error: 'github_verification_failed',
|
|
276
|
+
message: 'Could not verify key against this GitHub account',
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check if an existing tenant is associated with this GitHub username (auto-link)
|
|
282
|
+
const existingTenantId = await findTenantByGitHub(request.github, ddb, tableName);
|
|
283
|
+
if (existingTenantId) {
|
|
284
|
+
// Fresh-fetch GitHub keys (bypass cache) to ensure both device keys still appear
|
|
285
|
+
let freshKeys: string[];
|
|
286
|
+
try {
|
|
287
|
+
freshKeys = await fetchGitHubKeysFresh(request.github);
|
|
288
|
+
} catch {
|
|
289
|
+
// GitHub unreachable at auto-link time — fall back to normal registration
|
|
290
|
+
freshKeys = [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Verify the new device's key appears on the GitHub account (fresh data)
|
|
294
|
+
if (freshKeys.length > 0 && !keyAppearsInGitHubKeys(request.publicKey, freshKeys)) {
|
|
269
295
|
return {
|
|
270
296
|
statusCode: 400,
|
|
271
297
|
headers: JSON_HEADERS,
|
|
272
298
|
body: JSON.stringify({
|
|
273
|
-
error: '
|
|
274
|
-
message:
|
|
299
|
+
error: 'github_verification_failed',
|
|
300
|
+
message: 'Could not verify key against this GitHub account',
|
|
275
301
|
}),
|
|
276
302
|
};
|
|
277
303
|
}
|
|
278
|
-
|
|
279
|
-
if (
|
|
304
|
+
|
|
305
|
+
if (freshKeys.length > 0) {
|
|
306
|
+
// Resolve location from CloudFront headers for the notification
|
|
307
|
+
const location = resolveIpLocation(headers);
|
|
308
|
+
const deviceInfo: DeviceInfo = {
|
|
309
|
+
...request.deviceInfo,
|
|
310
|
+
location,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Create notification for existing devices
|
|
314
|
+
await createNotification(existingTenantId, 'device_linked', deviceInfo, ddb, tableName);
|
|
315
|
+
|
|
316
|
+
// Audit event
|
|
317
|
+
await logAuditEvent(ddb, tableName, existingTenantId, {
|
|
318
|
+
eventType: 'device-linked',
|
|
319
|
+
fingerprint: '',
|
|
320
|
+
metadata: {
|
|
321
|
+
publicKey: request.publicKey,
|
|
322
|
+
github: request.github,
|
|
323
|
+
...(deviceInfo.hostname && { hostname: deviceInfo.hostname }),
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
logger.info('GitHub auto-link: existing tenant found', {
|
|
328
|
+
existingTenantId,
|
|
329
|
+
github: request.github,
|
|
330
|
+
});
|
|
280
331
|
return {
|
|
281
|
-
statusCode:
|
|
332
|
+
statusCode: 200,
|
|
282
333
|
headers: JSON_HEADERS,
|
|
283
|
-
body: JSON.stringify({
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
status: 'auto_linked',
|
|
336
|
+
github: request.github,
|
|
337
|
+
}),
|
|
284
338
|
};
|
|
285
339
|
}
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Check if an existing tenant is associated with this GitHub username (auto-link)
|
|
290
|
-
const existingTenantId = await findTenantByGitHub(request.github, ddb, tableName);
|
|
291
|
-
if (existingTenantId) {
|
|
292
|
-
logger.info('GitHub auto-link: existing tenant found', {
|
|
293
|
-
existingTenantId,
|
|
294
|
-
github: request.github,
|
|
295
|
-
});
|
|
296
|
-
return {
|
|
297
|
-
statusCode: 200,
|
|
298
|
-
headers: JSON_HEADERS,
|
|
299
|
-
body: JSON.stringify({
|
|
300
|
-
status: 'auto_linked',
|
|
301
|
-
tenantId: existingTenantId,
|
|
302
|
-
github: request.github,
|
|
303
|
-
}),
|
|
304
|
-
};
|
|
340
|
+
// If fresh keys unavailable, fall through to normal registration
|
|
305
341
|
}
|
|
306
342
|
}
|
|
307
343
|
|
|
@@ -328,8 +364,19 @@ export async function handleRegister(
|
|
|
328
364
|
|
|
329
365
|
// Store GitHub association if provided
|
|
330
366
|
if (request.github) {
|
|
367
|
+
const claimed = await storeGitHubReverseLookup(request.github, tenantId, ddb, tableName);
|
|
368
|
+
if (!claimed) {
|
|
369
|
+
// Another tenant already claimed this GitHub username — uniform error
|
|
370
|
+
return {
|
|
371
|
+
statusCode: 400,
|
|
372
|
+
headers: JSON_HEADERS,
|
|
373
|
+
body: JSON.stringify({
|
|
374
|
+
error: 'github_verification_failed',
|
|
375
|
+
message: 'Could not verify key against this GitHub account',
|
|
376
|
+
}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
331
379
|
await storeGitHubAssociation(tenantId, request.github, ddb, tableName);
|
|
332
|
-
await storeGitHubReverseLookup(request.github, tenantId, ddb, tableName);
|
|
333
380
|
}
|
|
334
381
|
|
|
335
382
|
await logAuditEvent(ddb, tableName, tenantId, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA6B,MAAM,uBAAuB,CAAC;AAG1F,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA6B,MAAM,uBAAuB,CAAC;AAG1F,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAyD1B"}
|
|
@@ -31,8 +31,7 @@ async function handleRestore(blobId, tenantId, ddb, tableName) {
|
|
|
31
31
|
await ddb.send(new lib_dynamodb_1.UpdateCommand({
|
|
32
32
|
TableName: tableName,
|
|
33
33
|
Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
|
|
34
|
-
UpdateExpression: 'REMOVE deletedAt,
|
|
35
|
-
ExpressionAttributeNames: { '#ttl': 'ttl' },
|
|
34
|
+
UpdateExpression: 'REMOVE deletedAt, expiresAt SET updatedAt = :updatedAt',
|
|
36
35
|
ExpressionAttributeValues: { ':updatedAt': now },
|
|
37
36
|
}));
|
|
38
37
|
// Re-increment storage used
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore.js","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":";;AASA,
|
|
1
|
+
{"version":3,"file":"restore.js","sourceRoot":"","sources":["../../../../lib/handler/routes/restore.ts"],"names":[],"mappings":";;AASA,sCA8DC;AAvED,wDAA0F;AAC1F,4CAAsC;AAQ/B,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,QAAgB,EAChB,GAA2B,EAC3B,SAAiB;IAEjB,kDAAkD;IAClD,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,CAC7B,IAAI,yBAAU,CAAC;QACb,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,MAAM,EAAE,EAAE;QACvD,oBAAoB,EAAE,eAAe;QACrC,wBAAwB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;KAC3C,CAAC,CACH,CAAC;IAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;SACxE,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,OAAO;YACL,UAAU,EAAE,GAAG;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;SAC/E,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAY,IAAI,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,2BAA2B;IAC3B,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,4BAAa,CAAC;QAChB,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,MAAM,EAAE,EAAE;QACvD,gBAAgB,EAAE,wDAAwD;QAC1E,yBAAyB,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE;KACjD,CAAC,CACH,CAAC;IAEF,4BAA4B;IAC5B,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,4BAAa,CAAC;QAChB,SAAS,EAAE,SAAS;QACpB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;QAC7C,gBAAgB,EAAE,iDAAiD;QACnE,yBAAyB,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE;KACjD,CAAC,CACH,CAAC;IAEF,kBAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;IAEjE,OAAO;QACL,UAAU,EAAE,GAAG;QACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KACrD,CAAC;AACJ,CAAC"}
|
|
@@ -47,8 +47,7 @@ export async function handleRestore(
|
|
|
47
47
|
new UpdateCommand({
|
|
48
48
|
TableName: tableName,
|
|
49
49
|
Key: { PK: `TENANT#${tenantId}`, SK: `BLOB#${blobId}` },
|
|
50
|
-
UpdateExpression: 'REMOVE deletedAt,
|
|
51
|
-
ExpressionAttributeNames: { '#ttl': 'ttl' },
|
|
50
|
+
UpdateExpression: 'REMOVE deletedAt, expiresAt SET updatedAt = :updatedAt',
|
|
52
51
|
ExpressionAttributeValues: { ':updatedAt': now },
|
|
53
52
|
}),
|
|
54
53
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/rotation.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAG/B,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AA6BD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/rotation.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAG/B,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AA6BD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAoI1B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA6F1B"}
|
|
@@ -44,8 +44,7 @@ function fingerprintFromPublicKey(publicKeyBase64) {
|
|
|
44
44
|
return crypto.createHash('sha256').update(Buffer.from(publicKeyBase64, 'base64')).digest('base64');
|
|
45
45
|
}
|
|
46
46
|
function tenantIdFromPublicKey(publicKeyBase64) {
|
|
47
|
-
|
|
48
|
-
return hash.slice(0, 32);
|
|
47
|
+
return crypto.createHash('sha256').update(publicKeyBase64).digest('hex').slice(0, 32);
|
|
49
48
|
}
|
|
50
49
|
function isValidBase64(value) {
|
|
51
50
|
if (!value || value.length < 4 || value.length > 8192)
|
|
@@ -153,6 +152,19 @@ async function handleRotateStart(tenantId, oldFingerprint, rawBody, ddb, tableNa
|
|
|
153
152
|
updatedAt: now,
|
|
154
153
|
},
|
|
155
154
|
}));
|
|
155
|
+
// Write a KEY_ALIAS record so auth middleware can resolve the new key's
|
|
156
|
+
// derived tenantId back to this tenant during rotation
|
|
157
|
+
const newKeyTenantId = tenantIdFromPublicKey(request.newPublicKey);
|
|
158
|
+
await ddb.send(new lib_dynamodb_1.PutCommand({
|
|
159
|
+
TableName: tableName,
|
|
160
|
+
Item: {
|
|
161
|
+
PK: `KEY_ALIAS#${newKeyTenantId}`,
|
|
162
|
+
SK: 'META',
|
|
163
|
+
originalTenantId: tenantId,
|
|
164
|
+
newPublicKey: request.newPublicKey,
|
|
165
|
+
createdAt: now,
|
|
166
|
+
},
|
|
167
|
+
}));
|
|
156
168
|
logger_js_1.logger.info('Key rotation started', { tenantId, oldFingerprint, newFingerprint });
|
|
157
169
|
return {
|
|
158
170
|
statusCode: 200,
|
|
@@ -287,5 +299,14 @@ async function completeRotation(tenantId, rotation, ddb, tableName) {
|
|
|
287
299
|
SK: 'ROTATION',
|
|
288
300
|
},
|
|
289
301
|
}));
|
|
302
|
+
// Delete the KEY_ALIAS lookup record for the new key
|
|
303
|
+
const newKeyTenantId = tenantIdFromPublicKey(newPublicKey);
|
|
304
|
+
await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
305
|
+
TableName: tableName,
|
|
306
|
+
Key: {
|
|
307
|
+
PK: `KEY_ALIAS#${newKeyTenantId}`,
|
|
308
|
+
SK: 'META',
|
|
309
|
+
},
|
|
310
|
+
}));
|
|
290
311
|
}
|
|
291
312
|
//# sourceMappingURL=rotation.js.map
|