@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/audits.d.ts +188 -0
  4. package/dist/audits.d.ts.map +1 -0
  5. package/dist/audits.js +225 -0
  6. package/dist/batch/batch-processor.d.ts +81 -0
  7. package/dist/batch/batch-processor.d.ts.map +1 -0
  8. package/dist/batch/batch-processor.js +102 -0
  9. package/dist/batch/index.d.ts +2 -0
  10. package/dist/batch/index.d.ts.map +1 -0
  11. package/dist/batch/index.js +1 -0
  12. package/dist/config.d.ts +65 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +44 -0
  15. package/dist/constants.d.ts +147 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +191 -0
  18. package/dist/events/batch-handler.d.ts +62 -0
  19. package/dist/events/batch-handler.d.ts.map +1 -0
  20. package/dist/events/batch-handler.js +92 -0
  21. package/dist/events/bus.d.ts +79 -0
  22. package/dist/events/bus.d.ts.map +1 -0
  23. package/dist/events/bus.js +86 -0
  24. package/dist/events/index.d.ts +3 -0
  25. package/dist/events/index.d.ts.map +1 -0
  26. package/dist/events/index.js +2 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/middleware/middy.d.ts +66 -0
  31. package/dist/middleware/middy.d.ts.map +1 -0
  32. package/dist/middleware/middy.js +105 -0
  33. package/dist/repository.d.ts +421 -0
  34. package/dist/repository.d.ts.map +1 -0
  35. package/dist/repository.js +486 -0
  36. package/dist/repository.utils.d.ts +37 -0
  37. package/dist/repository.utils.d.ts.map +1 -0
  38. package/dist/repository.utils.js +70 -0
  39. package/dist/schema/audit.d.ts +202 -0
  40. package/dist/schema/audit.d.ts.map +1 -0
  41. package/dist/schema/audit.js +48 -0
  42. package/dist/schema/common.d.ts +128 -0
  43. package/dist/schema/common.d.ts.map +1 -0
  44. package/dist/schema/common.js +148 -0
  45. package/dist/schema/index.d.ts +27 -0
  46. package/dist/schema/index.d.ts.map +1 -0
  47. package/dist/schema/index.js +26 -0
  48. package/dist/schema/log.d.ts +211 -0
  49. package/dist/schema/log.d.ts.map +1 -0
  50. package/dist/schema/log.js +179 -0
  51. package/dist/schema/model.d.ts +110 -0
  52. package/dist/schema/model.d.ts.map +1 -0
  53. package/dist/schema/model.js +86 -0
  54. package/dist/schema/service.d.ts +189 -0
  55. package/dist/schema/service.d.ts.map +1 -0
  56. package/dist/schema/service.js +89 -0
  57. package/dist/schema/storage.d.ts +105 -0
  58. package/dist/schema/storage.d.ts.map +1 -0
  59. package/dist/schema/storage.js +63 -0
  60. package/dist/service.d.ts +285 -0
  61. package/dist/service.d.ts.map +1 -0
  62. package/dist/service.js +237 -0
  63. package/dist/test-config.d.ts +56 -0
  64. package/dist/test-config.d.ts.map +1 -0
  65. package/dist/test-config.js +24 -0
  66. package/dist/types.d.ts +40 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +1 -0
  69. package/dist/utils.d.ts +231 -0
  70. package/dist/utils.d.ts.map +1 -0
  71. package/dist/utils.js +313 -0
  72. 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
+ }