@flipboxlabs/aws-audit-sdk 1.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/audits.d.ts +188 -0
- package/dist/audits.d.ts.map +1 -0
- package/dist/audits.js +225 -0
- package/dist/batch/batch-processor.d.ts +81 -0
- package/dist/batch/batch-processor.d.ts.map +1 -0
- package/dist/batch/batch-processor.js +102 -0
- package/dist/batch/index.d.ts +2 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/constants.d.ts +147 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +191 -0
- package/dist/events/batch-handler.d.ts +62 -0
- package/dist/events/batch-handler.d.ts.map +1 -0
- package/dist/events/batch-handler.js +92 -0
- package/dist/events/bus.d.ts +79 -0
- package/dist/events/bus.d.ts.map +1 -0
- package/dist/events/bus.js +86 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/middleware/middy.d.ts +66 -0
- package/dist/middleware/middy.d.ts.map +1 -0
- package/dist/middleware/middy.js +105 -0
- package/dist/repository.d.ts +421 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +486 -0
- package/dist/repository.utils.d.ts +37 -0
- package/dist/repository.utils.d.ts.map +1 -0
- package/dist/repository.utils.js +70 -0
- package/dist/schema/audit.d.ts +202 -0
- package/dist/schema/audit.d.ts.map +1 -0
- package/dist/schema/audit.js +48 -0
- package/dist/schema/common.d.ts +128 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +148 -0
- package/dist/schema/index.d.ts +27 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +26 -0
- package/dist/schema/log.d.ts +211 -0
- package/dist/schema/log.d.ts.map +1 -0
- package/dist/schema/log.js +179 -0
- package/dist/schema/model.d.ts +110 -0
- package/dist/schema/model.d.ts.map +1 -0
- package/dist/schema/model.js +86 -0
- package/dist/schema/service.d.ts +189 -0
- package/dist/schema/service.d.ts.map +1 -0
- package/dist/schema/service.js +89 -0
- package/dist/schema/storage.d.ts +105 -0
- package/dist/schema/storage.d.ts.map +1 -0
- package/dist/schema/storage.js +63 -0
- package/dist/service.d.ts +285 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +237 -0
- package/dist/test-config.d.ts +56 -0
- package/dist/test-config.d.ts.map +1 -0
- package/dist/test-config.js +24 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +231 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +313 -0
- package/package.json +85 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { BatchWriteItemCommand, DynamoDBClient, GetItemCommand, QueryCommand, } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
|
|
3
|
+
import { DynamoDB } from "./constants.js";
|
|
4
|
+
import { decodeNextPageToken, encodeNextPageToken, } from "./repository.utils.js";
|
|
5
|
+
import { AuditListItemPayloadSchema, AuditPayloadSchema, AuditSchema, } from "./schema/audit.js";
|
|
6
|
+
import { PaginationCollectionSchema } from "./schema/model.js";
|
|
7
|
+
import { AuditStorageSchema } from "./schema/storage.js";
|
|
8
|
+
import { getTraceParts } from "./utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* Default TTL for audit records in seconds (90 days).
|
|
11
|
+
* Records will be automatically deleted by DynamoDB after this period.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_TTL = 60 * 60 * 24 * 90; // 90 days
|
|
14
|
+
/**
|
|
15
|
+
* Repository for managing audit records in DynamoDB.
|
|
16
|
+
*
|
|
17
|
+
* Provides CRUD operations for audit records with support for:
|
|
18
|
+
* - Single item retrieval by identifiers
|
|
19
|
+
* - Batch upsert operations with automatic batching
|
|
20
|
+
* - Paginated listing by resource
|
|
21
|
+
* - Paginated listing by trace ID for distributed tracing
|
|
22
|
+
*
|
|
23
|
+
* The repository uses a single-table design with composite keys and
|
|
24
|
+
* multiple GSI/LSI indexes for efficient query patterns.
|
|
25
|
+
*
|
|
26
|
+
* @typeParam C - The audit config type for type inference
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const config = defineAuditConfig({
|
|
31
|
+
* apps: ['Orders', 'Inventory'] as const,
|
|
32
|
+
* resourceTypes: ['Order', 'Product'] as const,
|
|
33
|
+
* });
|
|
34
|
+
* const repository = new AuditRepository(logger, config);
|
|
35
|
+
*
|
|
36
|
+
* // Get a single audit record - typed!
|
|
37
|
+
* const audit = await repository.getItem({
|
|
38
|
+
* id: 'audit-123',
|
|
39
|
+
* app: 'Orders', // TypeScript knows valid values
|
|
40
|
+
* resourceType: 'Order', // Autocomplete works
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Upsert multiple audit records
|
|
44
|
+
* await repository.upsertBatch([
|
|
45
|
+
* { operation: 'createUser', status: 'success', ... },
|
|
46
|
+
* { operation: 'updateUser', status: 'success', ... },
|
|
47
|
+
* ]);
|
|
48
|
+
*
|
|
49
|
+
* // List audits for a specific resource
|
|
50
|
+
* const { items, pagination } = await repository.listItems({
|
|
51
|
+
* app: 'Orders',
|
|
52
|
+
* resource: { type: 'Order', id: 'user-123' },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class AuditRepository {
|
|
57
|
+
logger;
|
|
58
|
+
config;
|
|
59
|
+
client;
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new AuditRepository instance.
|
|
62
|
+
*
|
|
63
|
+
* @param logger - Logger instance for error reporting and debugging
|
|
64
|
+
* @param config - Audit configuration for type inference
|
|
65
|
+
* @param client - DynamoDB client (defaults to new instance with AWS_REGION from environment)
|
|
66
|
+
*/
|
|
67
|
+
constructor(logger, config, client = new DynamoDBClient({
|
|
68
|
+
region: process.env.AWS_REGION ?? "us-east-1",
|
|
69
|
+
logger,
|
|
70
|
+
})) {
|
|
71
|
+
this.logger = logger;
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.client = client;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Retrieves a single audit record by its identifiers.
|
|
77
|
+
*
|
|
78
|
+
* Performs a DynamoDB GetItem operation using the constructed primary key.
|
|
79
|
+
* Returns undefined if the item is not found or if an error occurs.
|
|
80
|
+
*
|
|
81
|
+
* @param identifiers - Object containing id, app, and resourceType to locate the record
|
|
82
|
+
* @returns The audit record if found, undefined otherwise
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const audit = await repository.getItem({
|
|
87
|
+
* id: 'audit-123',
|
|
88
|
+
* app: 'Orders',
|
|
89
|
+
* resourceType: 'Order',
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* if (audit) {
|
|
93
|
+
* console.log('Found audit:', audit.operation);
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
async getItem(identifiers) {
|
|
98
|
+
try {
|
|
99
|
+
const { Item: item } = await this.client.send(new GetItemCommand({
|
|
100
|
+
TableName: DynamoDB.Table.Name(),
|
|
101
|
+
Key: marshall(this.constructPrimaryKey(identifiers), {
|
|
102
|
+
removeUndefinedValues: true,
|
|
103
|
+
convertEmptyValues: true,
|
|
104
|
+
convertClassInstanceToMap: true,
|
|
105
|
+
}),
|
|
106
|
+
}));
|
|
107
|
+
return item
|
|
108
|
+
? this.transformItem(unmarshall(item))
|
|
109
|
+
: undefined;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.logger.error("Unable to find audit item in the repository", {
|
|
113
|
+
identifiers,
|
|
114
|
+
error,
|
|
115
|
+
});
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Upserts multiple audit records in batch.
|
|
121
|
+
*
|
|
122
|
+
* Performs a DynamoDB BatchWriteItem operation, automatically chunking
|
|
123
|
+
* items into batches of 25 (DynamoDB's maximum batch size).
|
|
124
|
+
* Each item is enriched with:
|
|
125
|
+
* - Primary keys (PK, SK)
|
|
126
|
+
* - Secondary index keys (GSI1_SS, GSI1_SN, LSI1_N)
|
|
127
|
+
* - TTL attribute (90 days from creation)
|
|
128
|
+
* - Timestamps (createdAt, updatedAt)
|
|
129
|
+
*
|
|
130
|
+
* @param items - Array of audit records to upsert
|
|
131
|
+
* @returns True when the operation completes (does not check for unprocessed items)
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* await repository.upsertBatch([
|
|
136
|
+
* {
|
|
137
|
+
* operation: 'createUser',
|
|
138
|
+
* status: 'success',
|
|
139
|
+
* tier: 2,
|
|
140
|
+
* target: { app: App.App1, type: ResourceType.USER, id: 'user-123' },
|
|
141
|
+
* },
|
|
142
|
+
* {
|
|
143
|
+
* operation: 'updateUser',
|
|
144
|
+
* status: 'success',
|
|
145
|
+
* tier: 2,
|
|
146
|
+
* target: { app: App.App1, type: ResourceType.USER, id: 'user-456' },
|
|
147
|
+
* },
|
|
148
|
+
* ]);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
async upsertBatch(items) {
|
|
152
|
+
const BATCH_SIZE = 25;
|
|
153
|
+
const updatedAt = new Date();
|
|
154
|
+
const Items = await Promise.all(items.map((item) => {
|
|
155
|
+
const payload = AuditStorageSchema.parse({
|
|
156
|
+
createdAt: updatedAt,
|
|
157
|
+
...item,
|
|
158
|
+
updatedAt: updatedAt,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
PutRequest: {
|
|
162
|
+
Item: marshall({
|
|
163
|
+
...this.constructPrimaryKey({
|
|
164
|
+
tenantId: payload.tenantId,
|
|
165
|
+
id: payload.id,
|
|
166
|
+
resourceType: payload.target.type,
|
|
167
|
+
app: payload.target.app,
|
|
168
|
+
}),
|
|
169
|
+
...this.constructSecondaryKeys(payload),
|
|
170
|
+
...this.constructTTLAttribute(DEFAULT_TTL),
|
|
171
|
+
...payload,
|
|
172
|
+
}, {
|
|
173
|
+
removeUndefinedValues: true,
|
|
174
|
+
convertEmptyValues: true,
|
|
175
|
+
convertClassInstanceToMap: true,
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}));
|
|
180
|
+
const ItemBatches = [];
|
|
181
|
+
// Chunk into batches of 25 (DynamoDB BatchWriteItem limit)
|
|
182
|
+
for (let i = 0, j = Items.length; i < j; i += BATCH_SIZE) {
|
|
183
|
+
ItemBatches.push(Items.slice(i, i + BATCH_SIZE));
|
|
184
|
+
}
|
|
185
|
+
await Promise.all(ItemBatches.map((ItemBatch) => this.client.send(new BatchWriteItemCommand({
|
|
186
|
+
RequestItems: {
|
|
187
|
+
[DynamoDB.Table.Name()]: ItemBatch,
|
|
188
|
+
},
|
|
189
|
+
}))));
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Lists audit records for a specific resource with pagination.
|
|
194
|
+
*
|
|
195
|
+
* Queries the GSI1_SS index to efficiently retrieve all audits
|
|
196
|
+
* associated with a specific app/resource combination.
|
|
197
|
+
* Results are returned in reverse chronological order.
|
|
198
|
+
*
|
|
199
|
+
* @param params - Query parameters including app and resource identifiers
|
|
200
|
+
* @param pagination - Optional pagination settings (pageSize, nextToken)
|
|
201
|
+
* @returns Paginated collection of audit records
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // Get first page
|
|
206
|
+
* const page1 = await repository.listItems({
|
|
207
|
+
* app: 'Orders',
|
|
208
|
+
* resource: { type: 'Order', id: 'user-123' },
|
|
209
|
+
* });
|
|
210
|
+
*
|
|
211
|
+
* // Get next page using the pagination token
|
|
212
|
+
* const page2 = await repository.listItems(
|
|
213
|
+
* { app: 'Orders', resource: { type: 'Order', id: 'user-123' } },
|
|
214
|
+
* { nextToken: page1.pagination?.nextToken },
|
|
215
|
+
* );
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async listItems(params, pagination) {
|
|
219
|
+
const startKey = decodeNextPageToken(pagination?.nextToken);
|
|
220
|
+
const pageSize = Number(pagination?.pageSize || 100);
|
|
221
|
+
const pkBase = `${params.app}.${params.resource.type}#${params.resource.id}`;
|
|
222
|
+
const pk = params.tenantId ? `${params.tenantId}#${pkBase}` : pkBase;
|
|
223
|
+
const { Items: items, LastEvaluatedKey: lastEvaluatedKey } = await this.client.send(new QueryCommand({
|
|
224
|
+
ScanIndexForward: false,
|
|
225
|
+
IndexName: DynamoDB.Indexes.GSI1_SS,
|
|
226
|
+
ExpressionAttributeValues: marshall({
|
|
227
|
+
":PK": pk,
|
|
228
|
+
}),
|
|
229
|
+
ExpressionAttributeNames: {
|
|
230
|
+
"#PK": DynamoDB.Keys.GSI1_SS_PARTITION_KEY,
|
|
231
|
+
},
|
|
232
|
+
KeyConditionExpression: "#PK=:PK",
|
|
233
|
+
TableName: DynamoDB.Table.Name(),
|
|
234
|
+
Limit: pageSize,
|
|
235
|
+
ExclusiveStartKey: startKey
|
|
236
|
+
? marshall(startKey, {
|
|
237
|
+
removeUndefinedValues: true,
|
|
238
|
+
convertEmptyValues: true,
|
|
239
|
+
convertClassInstanceToMap: true,
|
|
240
|
+
})
|
|
241
|
+
: undefined,
|
|
242
|
+
}));
|
|
243
|
+
return PaginationCollectionSchema(AuditPayloadSchema).parse({
|
|
244
|
+
items: items?.map((i) => this.transformListItem(unmarshall(i))),
|
|
245
|
+
pagination: {
|
|
246
|
+
nextToken: lastEvaluatedKey
|
|
247
|
+
? encodeNextPageToken(unmarshall(lastEvaluatedKey))
|
|
248
|
+
: undefined,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Lists audit records by trace ID for distributed tracing.
|
|
254
|
+
*
|
|
255
|
+
* Queries the GSI1_SN index to retrieve all audits sharing
|
|
256
|
+
* the same trace ID, ordered by trace stage/depth.
|
|
257
|
+
* Supports optional filtering by app, resource type, and resource ID.
|
|
258
|
+
*
|
|
259
|
+
* This is useful for viewing the complete audit trail of a
|
|
260
|
+
* distributed operation across multiple services or resources.
|
|
261
|
+
*
|
|
262
|
+
* @param params - Query parameters including trace ID and optional filters
|
|
263
|
+
* @param pagination - Optional pagination settings (pageSize, nextToken)
|
|
264
|
+
* @returns Paginated collection of audit records in trace order
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* // Get all audits for a trace
|
|
269
|
+
* const trace = await repository.listTraceItems({
|
|
270
|
+
* trace: 'trace-abc-123',
|
|
271
|
+
* });
|
|
272
|
+
*
|
|
273
|
+
* // Filter by app and resource type
|
|
274
|
+
* const filtered = await repository.listTraceItems({
|
|
275
|
+
* trace: 'trace-abc-123',
|
|
276
|
+
* app: 'Orders',
|
|
277
|
+
* resource: { type: 'Order' },
|
|
278
|
+
* });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
async listTraceItems(params, pagination) {
|
|
282
|
+
const startKey = decodeNextPageToken(pagination?.nextToken);
|
|
283
|
+
const pageSize = Number(pagination?.pageSize || 100);
|
|
284
|
+
const pkBase = params.trace;
|
|
285
|
+
const pk = params.tenantId ? `${params.tenantId}#${pkBase}` : pkBase;
|
|
286
|
+
const filters = [];
|
|
287
|
+
const ExpressionAttributeValues = {
|
|
288
|
+
":PK": pk,
|
|
289
|
+
};
|
|
290
|
+
if (params?.resource?.id) {
|
|
291
|
+
Object.assign(ExpressionAttributeValues, {
|
|
292
|
+
":resourceId": params?.resource?.id,
|
|
293
|
+
});
|
|
294
|
+
filters.push("resourceId=:resourceId");
|
|
295
|
+
}
|
|
296
|
+
if (params?.resource?.type) {
|
|
297
|
+
Object.assign(ExpressionAttributeValues, {
|
|
298
|
+
":resourceType": params?.resource?.type,
|
|
299
|
+
});
|
|
300
|
+
filters.push("resourceType=:resourceType");
|
|
301
|
+
}
|
|
302
|
+
if (params?.app) {
|
|
303
|
+
Object.assign(ExpressionAttributeValues, {
|
|
304
|
+
":app": params.app,
|
|
305
|
+
});
|
|
306
|
+
filters.push("app=:app");
|
|
307
|
+
}
|
|
308
|
+
// Audits with other resources will have a compound id - filter these out
|
|
309
|
+
Object.assign(ExpressionAttributeValues, { ":isParent": "#" });
|
|
310
|
+
filters.push("not contains(SK, :isParent)");
|
|
311
|
+
const { Items: items, LastEvaluatedKey: lastEvaluatedKey } = await this.client.send(new QueryCommand({
|
|
312
|
+
ScanIndexForward: true,
|
|
313
|
+
IndexName: DynamoDB.Indexes.GSI1_SN,
|
|
314
|
+
ExpressionAttributeValues: marshall(ExpressionAttributeValues),
|
|
315
|
+
ExpressionAttributeNames: {
|
|
316
|
+
"#PK": DynamoDB.Keys.GSI1_SN_PARTITION_KEY,
|
|
317
|
+
},
|
|
318
|
+
KeyConditionExpression: "#PK=:PK",
|
|
319
|
+
TableName: DynamoDB.Table.Name(),
|
|
320
|
+
Limit: pageSize,
|
|
321
|
+
ExclusiveStartKey: startKey ? marshall(startKey) : undefined,
|
|
322
|
+
FilterExpression: filters.join(" AND "),
|
|
323
|
+
}));
|
|
324
|
+
return PaginationCollectionSchema(AuditPayloadSchema).parse({
|
|
325
|
+
items: items?.map((i) => this.transformTraceListItem(unmarshall(i))),
|
|
326
|
+
pagination: {
|
|
327
|
+
nextToken: lastEvaluatedKey
|
|
328
|
+
? encodeNextPageToken(unmarshall(lastEvaluatedKey))
|
|
329
|
+
: undefined,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Constructs the DynamoDB primary key from identifiers.
|
|
335
|
+
*
|
|
336
|
+
* When tenantId is provided, it prefixes the partition key for multi-tenant isolation.
|
|
337
|
+
*
|
|
338
|
+
* @param identifiers - Object containing tenantId (optional), app, id, and resourceType
|
|
339
|
+
* @returns Primary key object with PK and SK attributes
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
constructPrimaryKey({ tenantId, app, id, resourceType, }) {
|
|
343
|
+
const pkBase = `${app}.${resourceType}`;
|
|
344
|
+
return {
|
|
345
|
+
[DynamoDB.Keys.PARTITION_KEY]: (tenantId
|
|
346
|
+
? `${tenantId}#${pkBase}`
|
|
347
|
+
: pkBase),
|
|
348
|
+
[DynamoDB.Keys.SORT_KEY]: `${id}`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Constructs all secondary index keys for an audit item.
|
|
353
|
+
*
|
|
354
|
+
* Includes:
|
|
355
|
+
* - LSI1_N: Tier-based sort key for priority ordering
|
|
356
|
+
* - GSI1_SS: Resource-based index keys
|
|
357
|
+
* - GSI1_SN: Trace-based index keys (if trace exists)
|
|
358
|
+
*
|
|
359
|
+
* @param item - The audit storage item
|
|
360
|
+
* @returns Object containing all secondary key attributes
|
|
361
|
+
* @internal
|
|
362
|
+
*/
|
|
363
|
+
constructSecondaryKeys(item) {
|
|
364
|
+
return {
|
|
365
|
+
[DynamoDB.Keys.LSI1_N_SORT_KEY]: Number(`${item.tier}${Date.now()}`),
|
|
366
|
+
...this.constructGSI1_SS({
|
|
367
|
+
tenantId: item.tenantId,
|
|
368
|
+
id: item.id,
|
|
369
|
+
resourceType: item.target.type,
|
|
370
|
+
resourceId: item.target.id,
|
|
371
|
+
app: item.target.app,
|
|
372
|
+
}),
|
|
373
|
+
...this.constructGSI1_SN(item),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Constructs GSI1_SS (String-String) index keys for resource-based queries.
|
|
378
|
+
*
|
|
379
|
+
* The partition key combines app, resourceType, and resourceId for
|
|
380
|
+
* efficient querying of all audits for a specific resource.
|
|
381
|
+
* When tenantId is provided, it prefixes the partition key for multi-tenant isolation.
|
|
382
|
+
*
|
|
383
|
+
* @param identifiers - Full identifiers including resourceId and optional tenantId
|
|
384
|
+
* @returns GSI1_SS key attributes
|
|
385
|
+
* @internal
|
|
386
|
+
*/
|
|
387
|
+
constructGSI1_SS({ tenantId, app, id, resourceType, resourceId, }) {
|
|
388
|
+
const pkBase = [`${app}.${resourceType}`, resourceId]
|
|
389
|
+
.filter(Boolean)
|
|
390
|
+
.join("#");
|
|
391
|
+
return {
|
|
392
|
+
[DynamoDB.Keys.GSI1_SS_PARTITION_KEY]: (tenantId
|
|
393
|
+
? `${tenantId}#${pkBase}`
|
|
394
|
+
: pkBase),
|
|
395
|
+
[DynamoDB.Keys.GSI1_SS_SORT_KEY]: String(id),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Constructs GSI1_SN (String-Number) index keys for trace-based queries.
|
|
400
|
+
*
|
|
401
|
+
* Parses the trace string to extract the trace ID and stage number,
|
|
402
|
+
* enabling efficient querying of all audits in a distributed trace.
|
|
403
|
+
* When tenantId is provided, it prefixes the partition key for multi-tenant isolation.
|
|
404
|
+
*
|
|
405
|
+
* @param item - The audit storage item
|
|
406
|
+
* @returns GSI1_SN key attributes, or empty object if no trace
|
|
407
|
+
* @internal
|
|
408
|
+
*/
|
|
409
|
+
constructGSI1_SN(item) {
|
|
410
|
+
if (!item.trace) {
|
|
411
|
+
return {};
|
|
412
|
+
}
|
|
413
|
+
const { id, stage } = getTraceParts(item.trace);
|
|
414
|
+
const pkBase = `${id}`;
|
|
415
|
+
return {
|
|
416
|
+
[DynamoDB.Keys.GSI1_SN_PARTITION_KEY]: (item.tenantId
|
|
417
|
+
? `${item.tenantId}#${pkBase}`
|
|
418
|
+
: pkBase),
|
|
419
|
+
[DynamoDB.Keys.GSI1_SN_SORT_KEY]: stage,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Constructs the TTL attribute for automatic item expiration.
|
|
424
|
+
*
|
|
425
|
+
* @param ttlSeconds - Seconds until expiration (defaults to 90 days)
|
|
426
|
+
* @param date - Base date for TTL calculation (defaults to now)
|
|
427
|
+
* @returns TTL attribute with Unix timestamp in seconds
|
|
428
|
+
* @internal
|
|
429
|
+
*/
|
|
430
|
+
constructTTLAttribute(ttlSeconds = DEFAULT_TTL, date) {
|
|
431
|
+
const ttl = date || new Date();
|
|
432
|
+
if (ttlSeconds) {
|
|
433
|
+
ttl.setSeconds(ttl.getSeconds() + ttlSeconds);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ttl: Math.floor(ttl.valueOf() / 1000),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Transforms a raw DynamoDB item into an Audit object.
|
|
441
|
+
*
|
|
442
|
+
* Reconstructs the trace string and extracts the clean ID
|
|
443
|
+
* from compound keys.
|
|
444
|
+
*
|
|
445
|
+
* @param item - Raw DynamoDB item with all keys
|
|
446
|
+
* @returns Parsed and validated Audit object
|
|
447
|
+
* @internal
|
|
448
|
+
*/
|
|
449
|
+
transformItem(item) {
|
|
450
|
+
return Promise.resolve(AuditSchema.parse({
|
|
451
|
+
trace: `${item.GSI1_SN_PK}:${item.GSI1_SN_SK}`,
|
|
452
|
+
...item,
|
|
453
|
+
id: item.id.split("#").pop(),
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Transforms a trace listing item into an audit list item payload.
|
|
458
|
+
*
|
|
459
|
+
* @param item - Raw trace listing item from GSI1_SN query
|
|
460
|
+
* @returns Parsed audit list item payload
|
|
461
|
+
* @internal
|
|
462
|
+
*/
|
|
463
|
+
transformTraceListItem(item) {
|
|
464
|
+
const [id] = item.SK.split("#");
|
|
465
|
+
return AuditListItemPayloadSchema.parse({
|
|
466
|
+
trace: `${item.GSI1_SN_PK}:${item.GSI1_SN_SK}`,
|
|
467
|
+
...item,
|
|
468
|
+
id: id,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Transforms a resource listing item into an audit list item payload.
|
|
473
|
+
*
|
|
474
|
+
* @param item - Raw listing item from GSI1_SS query
|
|
475
|
+
* @returns Parsed audit list item payload
|
|
476
|
+
* @internal
|
|
477
|
+
*/
|
|
478
|
+
transformListItem(item) {
|
|
479
|
+
const [id] = item.SK.split("#");
|
|
480
|
+
return AuditListItemPayloadSchema.parse({
|
|
481
|
+
trace: `${item.GSI1_SN_PK}:${item.GSI1_SN_SK}`,
|
|
482
|
+
...item,
|
|
483
|
+
id: id,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes a pagination object into an encrypted token string.
|
|
3
|
+
*
|
|
4
|
+
* Uses AES-256-CTR encryption to obfuscate the DynamoDB LastEvaluatedKey,
|
|
5
|
+
* preventing clients from tampering with pagination state.
|
|
6
|
+
*
|
|
7
|
+
* The token format is: `{iv}:{encryptedData}` where both parts are hex-encoded.
|
|
8
|
+
*
|
|
9
|
+
* @param nextPageObject - DynamoDB LastEvaluatedKey object to encode
|
|
10
|
+
* @returns Encrypted pagination token string, or undefined if input is null/undefined
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const lastKey = { PK: 'App1.USER', SK: 'audit-123' };
|
|
15
|
+
* const token = encodeNextPageToken(lastKey);
|
|
16
|
+
* // Returns something like: "a1b2c3d4...:e5f6g7h8..."
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function encodeNextPageToken(nextPageObject?: Record<string, string>): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Decodes an encrypted pagination token back into a DynamoDB key object.
|
|
22
|
+
*
|
|
23
|
+
* Reverses the encryption performed by {@link encodeNextPageToken} to
|
|
24
|
+
* restore the original LastEvaluatedKey for DynamoDB queries.
|
|
25
|
+
*
|
|
26
|
+
* @param nextPageString - Encrypted pagination token from a previous response
|
|
27
|
+
* @returns Decoded DynamoDB key object, or undefined if input is null/undefined/invalid
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const token = "a1b2c3d4...:e5f6g7h8...";
|
|
32
|
+
* const lastKey = decodeNextPageToken(token);
|
|
33
|
+
* // Returns: { PK: 'App1.USER', SK: 'audit-123' }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function decodeNextPageToken(nextPageString?: string | null | undefined): Record<string, string> | undefined;
|
|
37
|
+
//# sourceMappingURL=repository.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.utils.d.ts","sourceRoot":"","sources":["../src/repository.utils.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAClC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,MAAM,GAAG,SAAS,CAgBpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAClC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACxC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAuBpC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Encryption algorithm used for pagination tokens.
|
|
4
|
+
* AES-256-CTR provides symmetric encryption with counter mode.
|
|
5
|
+
*/
|
|
6
|
+
const ENCRYPTION_ALGORITHM = "aes-256-ctr";
|
|
7
|
+
/**
|
|
8
|
+
* Key size in bytes for AES-256 encryption.
|
|
9
|
+
*/
|
|
10
|
+
const KEY_SIZE = 32;
|
|
11
|
+
/**
|
|
12
|
+
* Encodes a pagination object into an encrypted token string.
|
|
13
|
+
*
|
|
14
|
+
* Uses AES-256-CTR encryption to obfuscate the DynamoDB LastEvaluatedKey,
|
|
15
|
+
* preventing clients from tampering with pagination state.
|
|
16
|
+
*
|
|
17
|
+
* The token format is: `{iv}:{encryptedData}` where both parts are hex-encoded.
|
|
18
|
+
*
|
|
19
|
+
* @param nextPageObject - DynamoDB LastEvaluatedKey object to encode
|
|
20
|
+
* @returns Encrypted pagination token string, or undefined if input is null/undefined
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const lastKey = { PK: 'App1.USER', SK: 'audit-123' };
|
|
25
|
+
* const token = encodeNextPageToken(lastKey);
|
|
26
|
+
* // Returns something like: "a1b2c3d4...:e5f6g7h8..."
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function encodeNextPageToken(nextPageObject) {
|
|
30
|
+
if (nextPageObject == null) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const nextPageString = JSON.stringify(nextPageObject);
|
|
34
|
+
const iv = crypto.randomBytes(16);
|
|
35
|
+
const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, Buffer.concat([Buffer.alloc(KEY_SIZE)], KEY_SIZE), iv);
|
|
36
|
+
const encrypted = cipher.update(nextPageString);
|
|
37
|
+
return [iv.toString("hex"), encrypted.toString("hex")].join(":");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decodes an encrypted pagination token back into a DynamoDB key object.
|
|
41
|
+
*
|
|
42
|
+
* Reverses the encryption performed by {@link encodeNextPageToken} to
|
|
43
|
+
* restore the original LastEvaluatedKey for DynamoDB queries.
|
|
44
|
+
*
|
|
45
|
+
* @param nextPageString - Encrypted pagination token from a previous response
|
|
46
|
+
* @returns Decoded DynamoDB key object, or undefined if input is null/undefined/invalid
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const token = "a1b2c3d4...:e5f6g7h8...";
|
|
51
|
+
* const lastKey = decodeNextPageToken(token);
|
|
52
|
+
* // Returns: { PK: 'App1.USER', SK: 'audit-123' }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function decodeNextPageToken(nextPageString) {
|
|
56
|
+
if (nextPageString == null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const textParts = nextPageString.split(":");
|
|
60
|
+
const iv = textParts.shift();
|
|
61
|
+
if (!iv) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, Buffer.concat([Buffer.alloc(KEY_SIZE)], KEY_SIZE), Buffer.from(iv, "hex"));
|
|
65
|
+
const decrypted = Buffer.concat([
|
|
66
|
+
decipher.update(Buffer.from(textParts.join(":"), "hex")),
|
|
67
|
+
decipher.final(),
|
|
68
|
+
]);
|
|
69
|
+
return JSON.parse(decrypted.toString());
|
|
70
|
+
}
|