@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,24 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
interface HandlerResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
body: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* POST /v1/rotate-start — Phase 1 of two-phase key rotation.
|
|
9
|
+
*
|
|
10
|
+
* Authenticated with the OLD key. Stores the new public key alongside the
|
|
11
|
+
* old key and marks the rotation as started. Both old and new keys are
|
|
12
|
+
* accepted for authentication going forward.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleRotateStart(tenantId: string, oldFingerprint: string, rawBody: string | null | undefined, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* POST /v1/rotate-confirm — Phase 2 of two-phase key rotation.
|
|
17
|
+
*
|
|
18
|
+
* Authenticated with the NEW key. Adds the device to the confirmations list.
|
|
19
|
+
* When all registered devices have confirmed (or the 48h timeout has elapsed),
|
|
20
|
+
* completes the rotation by removing the old key and rotation record.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleRotateConfirm(tenantId: string, fingerprint: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=rotation.d.ts.map
|
|
@@ -0,0 +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,CAoH1B;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"}
|
|
@@ -0,0 +1,291 @@
|
|
|
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.handleRotateStart = handleRotateStart;
|
|
37
|
+
exports.handleRotateConfirm = handleRotateConfirm;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
40
|
+
const logger_js_1 = require("../logger.js");
|
|
41
|
+
/** 48 hours in milliseconds — timeout for rotation confirmation phase. */
|
|
42
|
+
const ROTATION_TIMEOUT_MS = 48 * 60 * 60 * 1000;
|
|
43
|
+
function fingerprintFromPublicKey(publicKeyBase64) {
|
|
44
|
+
return crypto.createHash('sha256').update(Buffer.from(publicKeyBase64, 'base64')).digest('base64');
|
|
45
|
+
}
|
|
46
|
+
function tenantIdFromPublicKey(publicKeyBase64) {
|
|
47
|
+
const hash = crypto.createHash('sha256').update(publicKeyBase64).digest('hex');
|
|
48
|
+
return hash.slice(0, 32);
|
|
49
|
+
}
|
|
50
|
+
function isValidBase64(value) {
|
|
51
|
+
if (!value || value.length < 4 || value.length > 8192)
|
|
52
|
+
return false;
|
|
53
|
+
try {
|
|
54
|
+
const decoded = Buffer.from(value, 'base64');
|
|
55
|
+
return decoded.length > 0;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* POST /v1/rotate-start — Phase 1 of two-phase key rotation.
|
|
63
|
+
*
|
|
64
|
+
* Authenticated with the OLD key. Stores the new public key alongside the
|
|
65
|
+
* old key and marks the rotation as started. Both old and new keys are
|
|
66
|
+
* accepted for authentication going forward.
|
|
67
|
+
*/
|
|
68
|
+
async function handleRotateStart(tenantId, oldFingerprint, rawBody, ddb, tableName) {
|
|
69
|
+
if (!rawBody) {
|
|
70
|
+
return {
|
|
71
|
+
statusCode: 400,
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
let request;
|
|
77
|
+
try {
|
|
78
|
+
request = JSON.parse(rawBody);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return {
|
|
82
|
+
statusCode: 400,
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!request.newPublicKey || !isValidBase64(request.newPublicKey)) {
|
|
88
|
+
return {
|
|
89
|
+
statusCode: 400,
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'newPublicKey is required and must be valid base64' }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (!request.wrappedBlob || !isValidBase64(request.wrappedBlob)) {
|
|
95
|
+
return {
|
|
96
|
+
statusCode: 400,
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'wrappedBlob is required and must be valid base64' }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Check for existing rotation in progress
|
|
102
|
+
const existingRotation = await ddb.send(new lib_dynamodb_1.GetCommand({
|
|
103
|
+
TableName: tableName,
|
|
104
|
+
Key: {
|
|
105
|
+
PK: `TENANT#${tenantId}`,
|
|
106
|
+
SK: 'ROTATION',
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
if (existingRotation.Item) {
|
|
110
|
+
return {
|
|
111
|
+
statusCode: 409,
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ error: 'rotation_in_progress', message: 'A key rotation is already in progress' }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const newFingerprint = fingerprintFromPublicKey(request.newPublicKey);
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
// Store rotation record
|
|
119
|
+
await ddb.send(new lib_dynamodb_1.PutCommand({
|
|
120
|
+
TableName: tableName,
|
|
121
|
+
Item: {
|
|
122
|
+
PK: `TENANT#${tenantId}`,
|
|
123
|
+
SK: 'ROTATION',
|
|
124
|
+
phase: 'started',
|
|
125
|
+
newPublicKey: request.newPublicKey,
|
|
126
|
+
newFingerprint,
|
|
127
|
+
oldFingerprint,
|
|
128
|
+
startedAt: now,
|
|
129
|
+
confirmations: [],
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
// Store the new public key in tenant META so auth middleware accepts both keys
|
|
133
|
+
await ddb.send(new lib_dynamodb_1.UpdateCommand({
|
|
134
|
+
TableName: tableName,
|
|
135
|
+
Key: {
|
|
136
|
+
PK: `TENANT#${tenantId}`,
|
|
137
|
+
SK: 'META',
|
|
138
|
+
},
|
|
139
|
+
UpdateExpression: 'SET newPublicKey = :npk, rotationState = :state, updatedAt = :now',
|
|
140
|
+
ExpressionAttributeValues: {
|
|
141
|
+
':npk': request.newPublicKey,
|
|
142
|
+
':state': 'ROTATION_STARTED',
|
|
143
|
+
':now': now,
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
// Store the wrapped blob for the new key
|
|
147
|
+
await ddb.send(new lib_dynamodb_1.PutCommand({
|
|
148
|
+
TableName: tableName,
|
|
149
|
+
Item: {
|
|
150
|
+
PK: `TENANT#${tenantId}`,
|
|
151
|
+
SK: `WRAPPED_KEY#${newFingerprint}`,
|
|
152
|
+
data: request.wrappedBlob,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
logger_js_1.logger.info('Key rotation started', { tenantId, oldFingerprint, newFingerprint });
|
|
157
|
+
return {
|
|
158
|
+
statusCode: 200,
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
status: 'rotation_started',
|
|
162
|
+
newFingerprint,
|
|
163
|
+
oldFingerprint,
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* POST /v1/rotate-confirm — Phase 2 of two-phase key rotation.
|
|
169
|
+
*
|
|
170
|
+
* Authenticated with the NEW key. Adds the device to the confirmations list.
|
|
171
|
+
* When all registered devices have confirmed (or the 48h timeout has elapsed),
|
|
172
|
+
* completes the rotation by removing the old key and rotation record.
|
|
173
|
+
*/
|
|
174
|
+
async function handleRotateConfirm(tenantId, fingerprint, ddb, tableName) {
|
|
175
|
+
// Get the rotation record
|
|
176
|
+
const rotationResult = await ddb.send(new lib_dynamodb_1.GetCommand({
|
|
177
|
+
TableName: tableName,
|
|
178
|
+
Key: {
|
|
179
|
+
PK: `TENANT#${tenantId}`,
|
|
180
|
+
SK: 'ROTATION',
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
if (!rotationResult.Item) {
|
|
184
|
+
return {
|
|
185
|
+
statusCode: 404,
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({ error: 'not_found', message: 'No key rotation in progress' }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const rotation = rotationResult.Item;
|
|
191
|
+
const confirmations = rotation['confirmations'] ?? [];
|
|
192
|
+
// Add this device fingerprint to confirmations if not already present
|
|
193
|
+
if (!confirmations.includes(fingerprint)) {
|
|
194
|
+
confirmations.push(fingerprint);
|
|
195
|
+
await ddb.send(new lib_dynamodb_1.UpdateCommand({
|
|
196
|
+
TableName: tableName,
|
|
197
|
+
Key: {
|
|
198
|
+
PK: `TENANT#${tenantId}`,
|
|
199
|
+
SK: 'ROTATION',
|
|
200
|
+
},
|
|
201
|
+
UpdateExpression: 'SET confirmations = :c',
|
|
202
|
+
ExpressionAttributeValues: {
|
|
203
|
+
':c': confirmations,
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
// Check if rotation should be completed:
|
|
208
|
+
// Either all devices confirmed or 48h timeout elapsed
|
|
209
|
+
const startedAt = new Date(rotation['startedAt']).getTime();
|
|
210
|
+
const timeoutReached = Date.now() - startedAt > ROTATION_TIMEOUT_MS;
|
|
211
|
+
// Count registered devices (WRAPPED_KEY# entries = registered devices)
|
|
212
|
+
const deviceResult = await ddb.send(new lib_dynamodb_1.QueryCommand({
|
|
213
|
+
TableName: tableName,
|
|
214
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
|
|
215
|
+
ExpressionAttributeValues: {
|
|
216
|
+
':pk': `TENANT#${tenantId}`,
|
|
217
|
+
':prefix': 'WRAPPED_KEY#',
|
|
218
|
+
},
|
|
219
|
+
Select: 'COUNT',
|
|
220
|
+
}));
|
|
221
|
+
const deviceCount = deviceResult.Count ?? 0;
|
|
222
|
+
const allConfirmed = confirmations.length >= deviceCount;
|
|
223
|
+
if (allConfirmed || timeoutReached) {
|
|
224
|
+
// Complete the rotation: Phase 2
|
|
225
|
+
await completeRotation(tenantId, rotation, ddb, tableName);
|
|
226
|
+
logger_js_1.logger.info('Key rotation completed', {
|
|
227
|
+
tenantId,
|
|
228
|
+
newFingerprint: rotation['newFingerprint'],
|
|
229
|
+
reason: timeoutReached ? 'timeout' : 'all_confirmed',
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
statusCode: 200,
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
status: 'rotation_complete',
|
|
236
|
+
confirmations: confirmations.length,
|
|
237
|
+
totalDevices: deviceCount,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
statusCode: 200,
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
status: 'confirmation_recorded',
|
|
246
|
+
confirmations: confirmations.length,
|
|
247
|
+
totalDevices: deviceCount,
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Complete the rotation by:
|
|
253
|
+
* 1. Updating tenant META with the new public key as primary
|
|
254
|
+
* 2. Deleting old wrapped key blob
|
|
255
|
+
* 3. Deleting the rotation record
|
|
256
|
+
*/
|
|
257
|
+
async function completeRotation(tenantId, rotation, ddb, tableName) {
|
|
258
|
+
const newPublicKey = rotation['newPublicKey'];
|
|
259
|
+
const oldFingerprint = rotation['oldFingerprint'];
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
// Update tenant META: replace primary public key, clear rotation state
|
|
262
|
+
await ddb.send(new lib_dynamodb_1.UpdateCommand({
|
|
263
|
+
TableName: tableName,
|
|
264
|
+
Key: {
|
|
265
|
+
PK: `TENANT#${tenantId}`,
|
|
266
|
+
SK: 'META',
|
|
267
|
+
},
|
|
268
|
+
UpdateExpression: 'SET publicKey = :pk, updatedAt = :now REMOVE newPublicKey, rotationState',
|
|
269
|
+
ExpressionAttributeValues: {
|
|
270
|
+
':pk': newPublicKey,
|
|
271
|
+
':now': now,
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
274
|
+
// Delete old wrapped key blob
|
|
275
|
+
await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
276
|
+
TableName: tableName,
|
|
277
|
+
Key: {
|
|
278
|
+
PK: `TENANT#${tenantId}`,
|
|
279
|
+
SK: `WRAPPED_KEY#${oldFingerprint}`,
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
// Delete rotation record
|
|
283
|
+
await ddb.send(new lib_dynamodb_1.DeleteCommand({
|
|
284
|
+
TableName: tableName,
|
|
285
|
+
Key: {
|
|
286
|
+
PK: `TENANT#${tenantId}`,
|
|
287
|
+
SK: 'ROTATION',
|
|
288
|
+
},
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=rotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation.js","sourceRoot":"","sources":["../../../../lib/handler/routes/rotation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
DynamoDBDocumentClient,
|
|
4
|
+
PutCommand,
|
|
5
|
+
GetCommand,
|
|
6
|
+
UpdateCommand,
|
|
7
|
+
DeleteCommand,
|
|
8
|
+
QueryCommand,
|
|
9
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
10
|
+
import { logger } from '../logger.js';
|
|
11
|
+
|
|
12
|
+
interface HandlerResponse {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
body: string;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RotateStartRequest {
|
|
19
|
+
newPublicKey: string;
|
|
20
|
+
wrappedBlob: string; // base64-encoded wrapped master key blob
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 48 hours in milliseconds — timeout for rotation confirmation phase. */
|
|
24
|
+
const ROTATION_TIMEOUT_MS = 48 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
function fingerprintFromPublicKey(publicKeyBase64: string): string {
|
|
27
|
+
return crypto.createHash('sha256').update(Buffer.from(publicKeyBase64, 'base64')).digest('base64');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tenantIdFromPublicKey(publicKeyBase64: string): string {
|
|
31
|
+
const hash = crypto.createHash('sha256').update(publicKeyBase64).digest('hex');
|
|
32
|
+
return hash.slice(0, 32);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isValidBase64(value: string): boolean {
|
|
36
|
+
if (!value || value.length < 4 || value.length > 8192) return false;
|
|
37
|
+
try {
|
|
38
|
+
const decoded = Buffer.from(value, 'base64');
|
|
39
|
+
return decoded.length > 0;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* POST /v1/rotate-start — Phase 1 of two-phase key rotation.
|
|
47
|
+
*
|
|
48
|
+
* Authenticated with the OLD key. Stores the new public key alongside the
|
|
49
|
+
* old key and marks the rotation as started. Both old and new keys are
|
|
50
|
+
* accepted for authentication going forward.
|
|
51
|
+
*/
|
|
52
|
+
export async function handleRotateStart(
|
|
53
|
+
tenantId: string,
|
|
54
|
+
oldFingerprint: string,
|
|
55
|
+
rawBody: string | null | undefined,
|
|
56
|
+
ddb: DynamoDBDocumentClient,
|
|
57
|
+
tableName: string,
|
|
58
|
+
): Promise<HandlerResponse> {
|
|
59
|
+
if (!rawBody) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'Request body is required' }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let request: RotateStartRequest;
|
|
68
|
+
try {
|
|
69
|
+
request = JSON.parse(rawBody);
|
|
70
|
+
} catch {
|
|
71
|
+
return {
|
|
72
|
+
statusCode: 400,
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'Invalid JSON body' }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!request.newPublicKey || !isValidBase64(request.newPublicKey)) {
|
|
79
|
+
return {
|
|
80
|
+
statusCode: 400,
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'newPublicKey is required and must be valid base64' }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!request.wrappedBlob || !isValidBase64(request.wrappedBlob)) {
|
|
87
|
+
return {
|
|
88
|
+
statusCode: 400,
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ error: 'invalid_request', message: 'wrappedBlob is required and must be valid base64' }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for existing rotation in progress
|
|
95
|
+
const existingRotation = await ddb.send(
|
|
96
|
+
new GetCommand({
|
|
97
|
+
TableName: tableName,
|
|
98
|
+
Key: {
|
|
99
|
+
PK: `TENANT#${tenantId}`,
|
|
100
|
+
SK: 'ROTATION',
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (existingRotation.Item) {
|
|
106
|
+
return {
|
|
107
|
+
statusCode: 409,
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ error: 'rotation_in_progress', message: 'A key rotation is already in progress' }),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const newFingerprint = fingerprintFromPublicKey(request.newPublicKey);
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
|
|
116
|
+
// Store rotation record
|
|
117
|
+
await ddb.send(
|
|
118
|
+
new PutCommand({
|
|
119
|
+
TableName: tableName,
|
|
120
|
+
Item: {
|
|
121
|
+
PK: `TENANT#${tenantId}`,
|
|
122
|
+
SK: 'ROTATION',
|
|
123
|
+
phase: 'started',
|
|
124
|
+
newPublicKey: request.newPublicKey,
|
|
125
|
+
newFingerprint,
|
|
126
|
+
oldFingerprint,
|
|
127
|
+
startedAt: now,
|
|
128
|
+
confirmations: [],
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Store the new public key in tenant META so auth middleware accepts both keys
|
|
134
|
+
await ddb.send(
|
|
135
|
+
new UpdateCommand({
|
|
136
|
+
TableName: tableName,
|
|
137
|
+
Key: {
|
|
138
|
+
PK: `TENANT#${tenantId}`,
|
|
139
|
+
SK: 'META',
|
|
140
|
+
},
|
|
141
|
+
UpdateExpression: 'SET newPublicKey = :npk, rotationState = :state, updatedAt = :now',
|
|
142
|
+
ExpressionAttributeValues: {
|
|
143
|
+
':npk': request.newPublicKey,
|
|
144
|
+
':state': 'ROTATION_STARTED',
|
|
145
|
+
':now': now,
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Store the wrapped blob for the new key
|
|
151
|
+
await ddb.send(
|
|
152
|
+
new PutCommand({
|
|
153
|
+
TableName: tableName,
|
|
154
|
+
Item: {
|
|
155
|
+
PK: `TENANT#${tenantId}`,
|
|
156
|
+
SK: `WRAPPED_KEY#${newFingerprint}`,
|
|
157
|
+
data: request.wrappedBlob,
|
|
158
|
+
updatedAt: now,
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
logger.info('Key rotation started', { tenantId, oldFingerprint, newFingerprint });
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
statusCode: 200,
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
status: 'rotation_started',
|
|
170
|
+
newFingerprint,
|
|
171
|
+
oldFingerprint,
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* POST /v1/rotate-confirm — Phase 2 of two-phase key rotation.
|
|
178
|
+
*
|
|
179
|
+
* Authenticated with the NEW key. Adds the device to the confirmations list.
|
|
180
|
+
* When all registered devices have confirmed (or the 48h timeout has elapsed),
|
|
181
|
+
* completes the rotation by removing the old key and rotation record.
|
|
182
|
+
*/
|
|
183
|
+
export async function handleRotateConfirm(
|
|
184
|
+
tenantId: string,
|
|
185
|
+
fingerprint: string,
|
|
186
|
+
ddb: DynamoDBDocumentClient,
|
|
187
|
+
tableName: string,
|
|
188
|
+
): Promise<HandlerResponse> {
|
|
189
|
+
// Get the rotation record
|
|
190
|
+
const rotationResult = await ddb.send(
|
|
191
|
+
new GetCommand({
|
|
192
|
+
TableName: tableName,
|
|
193
|
+
Key: {
|
|
194
|
+
PK: `TENANT#${tenantId}`,
|
|
195
|
+
SK: 'ROTATION',
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!rotationResult.Item) {
|
|
201
|
+
return {
|
|
202
|
+
statusCode: 404,
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({ error: 'not_found', message: 'No key rotation in progress' }),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const rotation = rotationResult.Item;
|
|
209
|
+
const confirmations: string[] = rotation['confirmations'] as string[] ?? [];
|
|
210
|
+
|
|
211
|
+
// Add this device fingerprint to confirmations if not already present
|
|
212
|
+
if (!confirmations.includes(fingerprint)) {
|
|
213
|
+
confirmations.push(fingerprint);
|
|
214
|
+
|
|
215
|
+
await ddb.send(
|
|
216
|
+
new UpdateCommand({
|
|
217
|
+
TableName: tableName,
|
|
218
|
+
Key: {
|
|
219
|
+
PK: `TENANT#${tenantId}`,
|
|
220
|
+
SK: 'ROTATION',
|
|
221
|
+
},
|
|
222
|
+
UpdateExpression: 'SET confirmations = :c',
|
|
223
|
+
ExpressionAttributeValues: {
|
|
224
|
+
':c': confirmations,
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if rotation should be completed:
|
|
231
|
+
// Either all devices confirmed or 48h timeout elapsed
|
|
232
|
+
const startedAt = new Date(rotation['startedAt'] as string).getTime();
|
|
233
|
+
const timeoutReached = Date.now() - startedAt > ROTATION_TIMEOUT_MS;
|
|
234
|
+
|
|
235
|
+
// Count registered devices (WRAPPED_KEY# entries = registered devices)
|
|
236
|
+
const deviceResult = await ddb.send(
|
|
237
|
+
new QueryCommand({
|
|
238
|
+
TableName: tableName,
|
|
239
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
|
|
240
|
+
ExpressionAttributeValues: {
|
|
241
|
+
':pk': `TENANT#${tenantId}`,
|
|
242
|
+
':prefix': 'WRAPPED_KEY#',
|
|
243
|
+
},
|
|
244
|
+
Select: 'COUNT',
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const deviceCount = deviceResult.Count ?? 0;
|
|
249
|
+
const allConfirmed = confirmations.length >= deviceCount;
|
|
250
|
+
|
|
251
|
+
if (allConfirmed || timeoutReached) {
|
|
252
|
+
// Complete the rotation: Phase 2
|
|
253
|
+
await completeRotation(tenantId, rotation, ddb, tableName);
|
|
254
|
+
|
|
255
|
+
logger.info('Key rotation completed', {
|
|
256
|
+
tenantId,
|
|
257
|
+
newFingerprint: rotation['newFingerprint'],
|
|
258
|
+
reason: timeoutReached ? 'timeout' : 'all_confirmed',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
statusCode: 200,
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
status: 'rotation_complete',
|
|
266
|
+
confirmations: confirmations.length,
|
|
267
|
+
totalDevices: deviceCount,
|
|
268
|
+
}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
statusCode: 200,
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
status: 'confirmation_recorded',
|
|
277
|
+
confirmations: confirmations.length,
|
|
278
|
+
totalDevices: deviceCount,
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Complete the rotation by:
|
|
285
|
+
* 1. Updating tenant META with the new public key as primary
|
|
286
|
+
* 2. Deleting old wrapped key blob
|
|
287
|
+
* 3. Deleting the rotation record
|
|
288
|
+
*/
|
|
289
|
+
async function completeRotation(
|
|
290
|
+
tenantId: string,
|
|
291
|
+
rotation: Record<string, unknown>,
|
|
292
|
+
ddb: DynamoDBDocumentClient,
|
|
293
|
+
tableName: string,
|
|
294
|
+
): Promise<void> {
|
|
295
|
+
const newPublicKey = rotation['newPublicKey'] as string;
|
|
296
|
+
const oldFingerprint = rotation['oldFingerprint'] as string;
|
|
297
|
+
const now = new Date().toISOString();
|
|
298
|
+
|
|
299
|
+
// Update tenant META: replace primary public key, clear rotation state
|
|
300
|
+
await ddb.send(
|
|
301
|
+
new UpdateCommand({
|
|
302
|
+
TableName: tableName,
|
|
303
|
+
Key: {
|
|
304
|
+
PK: `TENANT#${tenantId}`,
|
|
305
|
+
SK: 'META',
|
|
306
|
+
},
|
|
307
|
+
UpdateExpression: 'SET publicKey = :pk, updatedAt = :now REMOVE newPublicKey, rotationState',
|
|
308
|
+
ExpressionAttributeValues: {
|
|
309
|
+
':pk': newPublicKey,
|
|
310
|
+
':now': now,
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Delete old wrapped key blob
|
|
316
|
+
await ddb.send(
|
|
317
|
+
new DeleteCommand({
|
|
318
|
+
TableName: tableName,
|
|
319
|
+
Key: {
|
|
320
|
+
PK: `TENANT#${tenantId}`,
|
|
321
|
+
SK: `WRAPPED_KEY#${oldFingerprint}`,
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Delete rotation record
|
|
327
|
+
await ddb.send(
|
|
328
|
+
new DeleteCommand({
|
|
329
|
+
TableName: tableName,
|
|
330
|
+
Key: {
|
|
331
|
+
PK: `TENANT#${tenantId}`,
|
|
332
|
+
SK: 'ROTATION',
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 handleCreateTenant(body: string | null | undefined, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
8
|
+
export declare function handleListTenants(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
9
|
+
export declare function handleDeleteTenant(projectTenantId: string, tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<HandlerResponse>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=tenants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenants.d.ts","sourceRoot":"","sources":["../../../../lib/handler/routes/tenants.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAIvB,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;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAgE1B;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAuB1B;AAED,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA6E1B"}
|