@forklaunch/infrastructure-redis 1.2.10 → 1.3.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/lib/eject/infrastructure/redis.ts +67 -9
- package/lib/index.d.mts +22 -3
- package/lib/index.d.ts +22 -3
- package/lib/index.js +40 -8
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +42 -8
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
OpenTelemetryCollector,
|
|
7
7
|
TelemetryOptions
|
|
8
8
|
} from '@forklaunch/core/http';
|
|
9
|
+
import {
|
|
10
|
+
getCurrentTenantId,
|
|
11
|
+
type FieldEncryptor
|
|
12
|
+
} from '@forklaunch/core/persistence';
|
|
9
13
|
import { createClient, RedisClientOptions } from 'redis';
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -20,36 +24,81 @@ type RedisCommandRawReply =
|
|
|
20
24
|
| undefined
|
|
21
25
|
| Array<RedisCommandRawReply>;
|
|
22
26
|
|
|
27
|
+
const ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;
|
|
28
|
+
|
|
29
|
+
function isEncrypted(value: string): boolean {
|
|
30
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for configuring encryption on the Redis cache.
|
|
35
|
+
* Required — every consumer must explicitly configure encryption.
|
|
36
|
+
*/
|
|
37
|
+
export interface RedisCacheEncryptionOptions {
|
|
38
|
+
/** The FieldEncryptor instance to use for encrypting cache values. */
|
|
39
|
+
encryptor: FieldEncryptor;
|
|
40
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/**
|
|
24
45
|
* Class representing a Redis-based TTL (Time-To-Live) cache.
|
|
25
46
|
* Implements the TtlCache interface to provide caching functionality with automatic expiration.
|
|
47
|
+
*
|
|
48
|
+
* Encryption is enabled by default when an encryptor is provided. Values are encrypted
|
|
49
|
+
* before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.
|
|
26
50
|
*/
|
|
27
51
|
export class RedisTtlCache implements TtlCache {
|
|
28
52
|
private client;
|
|
29
53
|
private telemetryOptions;
|
|
54
|
+
private encryptor?: FieldEncryptor;
|
|
55
|
+
private encryptionDisabled: boolean;
|
|
30
56
|
|
|
31
57
|
/**
|
|
32
58
|
* Creates an instance of RedisTtlCache.
|
|
33
59
|
*
|
|
34
60
|
* @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries
|
|
35
61
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
36
|
-
* @param {RedisClientOptions}
|
|
62
|
+
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
37
63
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
64
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)
|
|
38
65
|
*/
|
|
39
66
|
constructor(
|
|
40
67
|
private ttlMilliseconds: number,
|
|
41
68
|
private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,
|
|
42
69
|
options: RedisClientOptions,
|
|
43
|
-
telemetryOptions: TelemetryOptions
|
|
70
|
+
telemetryOptions: TelemetryOptions,
|
|
71
|
+
encryption: RedisCacheEncryptionOptions
|
|
44
72
|
) {
|
|
45
73
|
this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);
|
|
46
74
|
this.client = createClient(options);
|
|
75
|
+
this.encryptor = encryption.encryptor;
|
|
76
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
47
77
|
if (this.telemetryOptions.enabled.logging) {
|
|
48
78
|
this.client.on('error', (err) => this.openTelemetryCollector.error(err));
|
|
49
79
|
this.client.connect().catch(this.openTelemetryCollector.error);
|
|
50
80
|
}
|
|
51
81
|
}
|
|
52
82
|
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Encryption helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
private encryptValue(serialized: string): string {
|
|
88
|
+
if (!this.encryptor || this.encryptionDisabled) return serialized;
|
|
89
|
+
return this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private decryptValue(value: string): string {
|
|
93
|
+
if (!this.encryptor || this.encryptionDisabled) return value;
|
|
94
|
+
if (!isEncrypted(value)) return value;
|
|
95
|
+
try {
|
|
96
|
+
return this.encryptor.decrypt(value, getCurrentTenantId()) ?? value;
|
|
97
|
+
} catch {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
53
102
|
/**
|
|
54
103
|
* Parses a raw Redis reply into the expected type.
|
|
55
104
|
* Handles null values, arrays, buffers, and JSON strings.
|
|
@@ -74,7 +123,7 @@ export class RedisTtlCache implements TtlCache {
|
|
|
74
123
|
switch (typeof value) {
|
|
75
124
|
case 'object':
|
|
76
125
|
case 'string':
|
|
77
|
-
return safeParse(value) as T;
|
|
126
|
+
return safeParse(this.decryptValue(String(value))) as T;
|
|
78
127
|
case 'number':
|
|
79
128
|
return value as T;
|
|
80
129
|
}
|
|
@@ -98,7 +147,7 @@ export class RedisTtlCache implements TtlCache {
|
|
|
98
147
|
if (this.telemetryOptions.enabled.logging) {
|
|
99
148
|
this.openTelemetryCollector.info(`Putting record into cache: ${key}`);
|
|
100
149
|
}
|
|
101
|
-
await this.client.set(key, safeStringify(value), {
|
|
150
|
+
await this.client.set(key, this.encryptValue(safeStringify(value)), {
|
|
102
151
|
PX: ttlMilliseconds
|
|
103
152
|
});
|
|
104
153
|
}
|
|
@@ -113,7 +162,7 @@ export class RedisTtlCache implements TtlCache {
|
|
|
113
162
|
async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {
|
|
114
163
|
const multiCommand = this.client.multi();
|
|
115
164
|
for (const { key, value, ttlMilliseconds } of cacheRecords) {
|
|
116
|
-
multiCommand.set(key, safeStringify(value), {
|
|
165
|
+
multiCommand.set(key, this.encryptValue(safeStringify(value)), {
|
|
117
166
|
PX: ttlMilliseconds || this.ttlMilliseconds
|
|
118
167
|
});
|
|
119
168
|
}
|
|
@@ -129,7 +178,10 @@ export class RedisTtlCache implements TtlCache {
|
|
|
129
178
|
* @returns {Promise<void>} A promise that resolves when the value is enqueued
|
|
130
179
|
*/
|
|
131
180
|
async enqueueRecord<T>(queueName: string, value: T): Promise<void> {
|
|
132
|
-
await this.client.lPush(
|
|
181
|
+
await this.client.lPush(
|
|
182
|
+
queueName,
|
|
183
|
+
this.encryptValue(safeStringify(value))
|
|
184
|
+
);
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
/**
|
|
@@ -143,7 +195,10 @@ export class RedisTtlCache implements TtlCache {
|
|
|
143
195
|
async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {
|
|
144
196
|
const multiCommand = this.client.multi();
|
|
145
197
|
for (const value of values) {
|
|
146
|
-
multiCommand.lPush(
|
|
198
|
+
multiCommand.lPush(
|
|
199
|
+
queueName,
|
|
200
|
+
this.encryptValue(safeStringify(value))
|
|
201
|
+
);
|
|
147
202
|
}
|
|
148
203
|
await multiCommand.exec();
|
|
149
204
|
}
|
|
@@ -185,7 +240,7 @@ export class RedisTtlCache implements TtlCache {
|
|
|
185
240
|
if (value === null) {
|
|
186
241
|
throw new Error(`Queue is empty: ${queueName}`);
|
|
187
242
|
}
|
|
188
|
-
return safeParse(value) as T;
|
|
243
|
+
return safeParse(this.decryptValue(value)) as T;
|
|
189
244
|
}
|
|
190
245
|
|
|
191
246
|
/**
|
|
@@ -339,7 +394,10 @@ export class RedisTtlCache implements TtlCache {
|
|
|
339
394
|
* @param {number} pageSize - The number of records to peek at
|
|
340
395
|
* @returns {Promise<T[]>} A promise that resolves with an array of peeked values
|
|
341
396
|
*/
|
|
342
|
-
async peekQueueRecords<T>(
|
|
397
|
+
async peekQueueRecords<T>(
|
|
398
|
+
queueName: string,
|
|
399
|
+
pageSize: number
|
|
400
|
+
): Promise<T[]> {
|
|
343
401
|
const values = await this.client.lRange(queueName, 0, pageSize - 1);
|
|
344
402
|
return values.map((value) => this.parseValue<T>(value)).filter(Boolean);
|
|
345
403
|
}
|
package/lib/index.d.mts
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
import { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';
|
|
2
2
|
import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
|
|
3
|
+
import { FieldEncryptor } from '@forklaunch/core/persistence';
|
|
3
4
|
import { RedisClientOptions } from 'redis';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring encryption on the Redis cache.
|
|
8
|
+
* Required — every consumer must explicitly configure encryption.
|
|
9
|
+
*/
|
|
10
|
+
interface RedisCacheEncryptionOptions {
|
|
11
|
+
/** The FieldEncryptor instance to use for encrypting cache values. */
|
|
12
|
+
encryptor: FieldEncryptor;
|
|
13
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
5
16
|
/**
|
|
6
17
|
* Class representing a Redis-based TTL (Time-To-Live) cache.
|
|
7
18
|
* Implements the TtlCache interface to provide caching functionality with automatic expiration.
|
|
19
|
+
*
|
|
20
|
+
* Encryption is enabled by default when an encryptor is provided. Values are encrypted
|
|
21
|
+
* before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.
|
|
8
22
|
*/
|
|
9
23
|
declare class RedisTtlCache implements TtlCache {
|
|
10
24
|
private ttlMilliseconds;
|
|
11
25
|
private openTelemetryCollector;
|
|
12
26
|
private client;
|
|
13
27
|
private telemetryOptions;
|
|
28
|
+
private encryptor?;
|
|
29
|
+
private encryptionDisabled;
|
|
14
30
|
/**
|
|
15
31
|
* Creates an instance of RedisTtlCache.
|
|
16
32
|
*
|
|
17
33
|
* @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries
|
|
18
34
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
19
|
-
* @param {RedisClientOptions}
|
|
35
|
+
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
20
36
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
37
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)
|
|
21
38
|
*/
|
|
22
|
-
constructor(ttlMilliseconds: number, openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: RedisClientOptions, telemetryOptions: TelemetryOptions);
|
|
39
|
+
constructor(ttlMilliseconds: number, openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: RedisClientOptions, telemetryOptions: TelemetryOptions, encryption: RedisCacheEncryptionOptions);
|
|
40
|
+
private encryptValue;
|
|
41
|
+
private decryptValue;
|
|
23
42
|
/**
|
|
24
43
|
* Parses a raw Redis reply into the expected type.
|
|
25
44
|
* Handles null values, arrays, buffers, and JSON strings.
|
|
@@ -173,4 +192,4 @@ declare class RedisTtlCache implements TtlCache {
|
|
|
173
192
|
getClient(): typeof this.client;
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
export { RedisTtlCache };
|
|
195
|
+
export { type RedisCacheEncryptionOptions, RedisTtlCache };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
import { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';
|
|
2
2
|
import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
|
|
3
|
+
import { FieldEncryptor } from '@forklaunch/core/persistence';
|
|
3
4
|
import { RedisClientOptions } from 'redis';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring encryption on the Redis cache.
|
|
8
|
+
* Required — every consumer must explicitly configure encryption.
|
|
9
|
+
*/
|
|
10
|
+
interface RedisCacheEncryptionOptions {
|
|
11
|
+
/** The FieldEncryptor instance to use for encrypting cache values. */
|
|
12
|
+
encryptor: FieldEncryptor;
|
|
13
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
5
16
|
/**
|
|
6
17
|
* Class representing a Redis-based TTL (Time-To-Live) cache.
|
|
7
18
|
* Implements the TtlCache interface to provide caching functionality with automatic expiration.
|
|
19
|
+
*
|
|
20
|
+
* Encryption is enabled by default when an encryptor is provided. Values are encrypted
|
|
21
|
+
* before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.
|
|
8
22
|
*/
|
|
9
23
|
declare class RedisTtlCache implements TtlCache {
|
|
10
24
|
private ttlMilliseconds;
|
|
11
25
|
private openTelemetryCollector;
|
|
12
26
|
private client;
|
|
13
27
|
private telemetryOptions;
|
|
28
|
+
private encryptor?;
|
|
29
|
+
private encryptionDisabled;
|
|
14
30
|
/**
|
|
15
31
|
* Creates an instance of RedisTtlCache.
|
|
16
32
|
*
|
|
17
33
|
* @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries
|
|
18
34
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
19
|
-
* @param {RedisClientOptions}
|
|
35
|
+
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
20
36
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
37
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)
|
|
21
38
|
*/
|
|
22
|
-
constructor(ttlMilliseconds: number, openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: RedisClientOptions, telemetryOptions: TelemetryOptions);
|
|
39
|
+
constructor(ttlMilliseconds: number, openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: RedisClientOptions, telemetryOptions: TelemetryOptions, encryption: RedisCacheEncryptionOptions);
|
|
40
|
+
private encryptValue;
|
|
41
|
+
private decryptValue;
|
|
23
42
|
/**
|
|
24
43
|
* Parses a raw Redis reply into the expected type.
|
|
25
44
|
* Handles null values, arrays, buffers, and JSON strings.
|
|
@@ -173,4 +192,4 @@ declare class RedisTtlCache implements TtlCache {
|
|
|
173
192
|
getClient(): typeof this.client;
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
export { RedisTtlCache };
|
|
195
|
+
export { type RedisCacheEncryptionOptions, RedisTtlCache };
|
package/lib/index.js
CHANGED
|
@@ -25,21 +25,29 @@ __export(index_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
var import_common = require("@forklaunch/common");
|
|
27
27
|
var import_http = require("@forklaunch/core/http");
|
|
28
|
+
var import_persistence = require("@forklaunch/core/persistence");
|
|
28
29
|
var import_redis = require("redis");
|
|
30
|
+
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
31
|
+
function isEncrypted(value) {
|
|
32
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
33
|
+
}
|
|
29
34
|
var RedisTtlCache = class {
|
|
30
35
|
/**
|
|
31
36
|
* Creates an instance of RedisTtlCache.
|
|
32
37
|
*
|
|
33
38
|
* @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries
|
|
34
39
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
35
|
-
* @param {RedisClientOptions}
|
|
40
|
+
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
36
41
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
42
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)
|
|
37
43
|
*/
|
|
38
|
-
constructor(ttlMilliseconds, openTelemetryCollector, options, telemetryOptions) {
|
|
44
|
+
constructor(ttlMilliseconds, openTelemetryCollector, options, telemetryOptions, encryption) {
|
|
39
45
|
this.ttlMilliseconds = ttlMilliseconds;
|
|
40
46
|
this.openTelemetryCollector = openTelemetryCollector;
|
|
41
47
|
this.telemetryOptions = (0, import_http.evaluateTelemetryOptions)(telemetryOptions);
|
|
42
48
|
this.client = (0, import_redis.createClient)(options);
|
|
49
|
+
this.encryptor = encryption.encryptor;
|
|
50
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
43
51
|
if (this.telemetryOptions.enabled.logging) {
|
|
44
52
|
this.client.on("error", (err) => this.openTelemetryCollector.error(err));
|
|
45
53
|
this.client.connect().catch(this.openTelemetryCollector.error);
|
|
@@ -47,6 +55,24 @@ var RedisTtlCache = class {
|
|
|
47
55
|
}
|
|
48
56
|
client;
|
|
49
57
|
telemetryOptions;
|
|
58
|
+
encryptor;
|
|
59
|
+
encryptionDisabled;
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Encryption helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
encryptValue(serialized) {
|
|
64
|
+
if (!this.encryptor || this.encryptionDisabled) return serialized;
|
|
65
|
+
return this.encryptor.encrypt(serialized, (0, import_persistence.getCurrentTenantId)()) ?? serialized;
|
|
66
|
+
}
|
|
67
|
+
decryptValue(value) {
|
|
68
|
+
if (!this.encryptor || this.encryptionDisabled) return value;
|
|
69
|
+
if (!isEncrypted(value)) return value;
|
|
70
|
+
try {
|
|
71
|
+
return this.encryptor.decrypt(value, (0, import_persistence.getCurrentTenantId)()) ?? value;
|
|
72
|
+
} catch {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
/**
|
|
51
77
|
* Parses a raw Redis reply into the expected type.
|
|
52
78
|
* Handles null values, arrays, buffers, and JSON strings.
|
|
@@ -68,7 +94,7 @@ var RedisTtlCache = class {
|
|
|
68
94
|
switch (typeof value) {
|
|
69
95
|
case "object":
|
|
70
96
|
case "string":
|
|
71
|
-
return (0, import_common.safeParse)(value);
|
|
97
|
+
return (0, import_common.safeParse)(this.decryptValue(String(value)));
|
|
72
98
|
case "number":
|
|
73
99
|
return value;
|
|
74
100
|
}
|
|
@@ -91,7 +117,7 @@ var RedisTtlCache = class {
|
|
|
91
117
|
if (this.telemetryOptions.enabled.logging) {
|
|
92
118
|
this.openTelemetryCollector.info(`Putting record into cache: ${key}`);
|
|
93
119
|
}
|
|
94
|
-
await this.client.set(key, (0, import_common.safeStringify)(value), {
|
|
120
|
+
await this.client.set(key, this.encryptValue((0, import_common.safeStringify)(value)), {
|
|
95
121
|
PX: ttlMilliseconds
|
|
96
122
|
});
|
|
97
123
|
}
|
|
@@ -105,7 +131,7 @@ var RedisTtlCache = class {
|
|
|
105
131
|
async putBatchRecords(cacheRecords) {
|
|
106
132
|
const multiCommand = this.client.multi();
|
|
107
133
|
for (const { key, value, ttlMilliseconds } of cacheRecords) {
|
|
108
|
-
multiCommand.set(key, (0, import_common.safeStringify)(value), {
|
|
134
|
+
multiCommand.set(key, this.encryptValue((0, import_common.safeStringify)(value)), {
|
|
109
135
|
PX: ttlMilliseconds || this.ttlMilliseconds
|
|
110
136
|
});
|
|
111
137
|
}
|
|
@@ -120,7 +146,10 @@ var RedisTtlCache = class {
|
|
|
120
146
|
* @returns {Promise<void>} A promise that resolves when the value is enqueued
|
|
121
147
|
*/
|
|
122
148
|
async enqueueRecord(queueName, value) {
|
|
123
|
-
await this.client.lPush(
|
|
149
|
+
await this.client.lPush(
|
|
150
|
+
queueName,
|
|
151
|
+
this.encryptValue((0, import_common.safeStringify)(value))
|
|
152
|
+
);
|
|
124
153
|
}
|
|
125
154
|
/**
|
|
126
155
|
* Adds multiple values to the left end of a Redis list in a single transaction.
|
|
@@ -133,7 +162,10 @@ var RedisTtlCache = class {
|
|
|
133
162
|
async enqueueBatchRecords(queueName, values) {
|
|
134
163
|
const multiCommand = this.client.multi();
|
|
135
164
|
for (const value of values) {
|
|
136
|
-
multiCommand.lPush(
|
|
165
|
+
multiCommand.lPush(
|
|
166
|
+
queueName,
|
|
167
|
+
this.encryptValue((0, import_common.safeStringify)(value))
|
|
168
|
+
);
|
|
137
169
|
}
|
|
138
170
|
await multiCommand.exec();
|
|
139
171
|
}
|
|
@@ -172,7 +204,7 @@ var RedisTtlCache = class {
|
|
|
172
204
|
if (value === null) {
|
|
173
205
|
throw new Error(`Queue is empty: ${queueName}`);
|
|
174
206
|
}
|
|
175
|
-
return (0, import_common.safeParse)(value);
|
|
207
|
+
return (0, import_common.safeParse)(this.decryptValue(value));
|
|
176
208
|
}
|
|
177
209
|
/**
|
|
178
210
|
* Removes and returns multiple elements from the right end of a Redis list.
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} hostingOptions - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(value) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, safeStringify(value), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, safeStringify(value), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(queueName, safeStringify(value));\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(queueName, safeStringify(value));\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(value) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(queueName: string, pageSize: number): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport {\n getCurrentTenantId,\n type FieldEncryptor\n} from '@forklaunch/core/persistence';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n/**\n * Options for configuring encryption on the Redis cache.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface RedisCacheEncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting cache values. */\n encryptor: FieldEncryptor;\n /** Set to true to disable encryption. Defaults to false (encryption enabled). */\n disabled?: boolean;\n}\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n *\n * Encryption is enabled by default when an encryptor is provided. Values are encrypted\n * before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n private encryptor?: FieldEncryptor;\n private encryptionDisabled: boolean;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} options - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n * @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions,\n encryption: RedisCacheEncryptionOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n this.encryptor = encryption.encryptor;\n this.encryptionDisabled = encryption.disabled ?? false;\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers\n // ---------------------------------------------------------------------------\n\n private encryptValue(serialized: string): string {\n if (!this.encryptor || this.encryptionDisabled) return serialized;\n return this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;\n }\n\n private decryptValue(value: string): string {\n if (!this.encryptor || this.encryptionDisabled) return value;\n if (!isEncrypted(value)) return value;\n try {\n return this.encryptor.decrypt(value, getCurrentTenantId()) ?? value;\n } catch {\n return value;\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(this.decryptValue(String(value))) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(\n queueName,\n this.encryptValue(safeStringify(value))\n );\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(\n queueName,\n this.encryptValue(safeStringify(value))\n );\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(this.decryptValue(value)) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":"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","names":[]}
|
package/lib/index.mjs
CHANGED
|
@@ -3,21 +3,31 @@ import { safeParse, safeStringify } from "@forklaunch/common";
|
|
|
3
3
|
import {
|
|
4
4
|
evaluateTelemetryOptions
|
|
5
5
|
} from "@forklaunch/core/http";
|
|
6
|
+
import {
|
|
7
|
+
getCurrentTenantId
|
|
8
|
+
} from "@forklaunch/core/persistence";
|
|
6
9
|
import { createClient } from "redis";
|
|
10
|
+
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
11
|
+
function isEncrypted(value) {
|
|
12
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
13
|
+
}
|
|
7
14
|
var RedisTtlCache = class {
|
|
8
15
|
/**
|
|
9
16
|
* Creates an instance of RedisTtlCache.
|
|
10
17
|
*
|
|
11
18
|
* @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries
|
|
12
19
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
13
|
-
* @param {RedisClientOptions}
|
|
20
|
+
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
14
21
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
22
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)
|
|
15
23
|
*/
|
|
16
|
-
constructor(ttlMilliseconds, openTelemetryCollector, options, telemetryOptions) {
|
|
24
|
+
constructor(ttlMilliseconds, openTelemetryCollector, options, telemetryOptions, encryption) {
|
|
17
25
|
this.ttlMilliseconds = ttlMilliseconds;
|
|
18
26
|
this.openTelemetryCollector = openTelemetryCollector;
|
|
19
27
|
this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);
|
|
20
28
|
this.client = createClient(options);
|
|
29
|
+
this.encryptor = encryption.encryptor;
|
|
30
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
21
31
|
if (this.telemetryOptions.enabled.logging) {
|
|
22
32
|
this.client.on("error", (err) => this.openTelemetryCollector.error(err));
|
|
23
33
|
this.client.connect().catch(this.openTelemetryCollector.error);
|
|
@@ -25,6 +35,24 @@ var RedisTtlCache = class {
|
|
|
25
35
|
}
|
|
26
36
|
client;
|
|
27
37
|
telemetryOptions;
|
|
38
|
+
encryptor;
|
|
39
|
+
encryptionDisabled;
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Encryption helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
encryptValue(serialized) {
|
|
44
|
+
if (!this.encryptor || this.encryptionDisabled) return serialized;
|
|
45
|
+
return this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;
|
|
46
|
+
}
|
|
47
|
+
decryptValue(value) {
|
|
48
|
+
if (!this.encryptor || this.encryptionDisabled) return value;
|
|
49
|
+
if (!isEncrypted(value)) return value;
|
|
50
|
+
try {
|
|
51
|
+
return this.encryptor.decrypt(value, getCurrentTenantId()) ?? value;
|
|
52
|
+
} catch {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
28
56
|
/**
|
|
29
57
|
* Parses a raw Redis reply into the expected type.
|
|
30
58
|
* Handles null values, arrays, buffers, and JSON strings.
|
|
@@ -46,7 +74,7 @@ var RedisTtlCache = class {
|
|
|
46
74
|
switch (typeof value) {
|
|
47
75
|
case "object":
|
|
48
76
|
case "string":
|
|
49
|
-
return safeParse(value);
|
|
77
|
+
return safeParse(this.decryptValue(String(value)));
|
|
50
78
|
case "number":
|
|
51
79
|
return value;
|
|
52
80
|
}
|
|
@@ -69,7 +97,7 @@ var RedisTtlCache = class {
|
|
|
69
97
|
if (this.telemetryOptions.enabled.logging) {
|
|
70
98
|
this.openTelemetryCollector.info(`Putting record into cache: ${key}`);
|
|
71
99
|
}
|
|
72
|
-
await this.client.set(key, safeStringify(value), {
|
|
100
|
+
await this.client.set(key, this.encryptValue(safeStringify(value)), {
|
|
73
101
|
PX: ttlMilliseconds
|
|
74
102
|
});
|
|
75
103
|
}
|
|
@@ -83,7 +111,7 @@ var RedisTtlCache = class {
|
|
|
83
111
|
async putBatchRecords(cacheRecords) {
|
|
84
112
|
const multiCommand = this.client.multi();
|
|
85
113
|
for (const { key, value, ttlMilliseconds } of cacheRecords) {
|
|
86
|
-
multiCommand.set(key, safeStringify(value), {
|
|
114
|
+
multiCommand.set(key, this.encryptValue(safeStringify(value)), {
|
|
87
115
|
PX: ttlMilliseconds || this.ttlMilliseconds
|
|
88
116
|
});
|
|
89
117
|
}
|
|
@@ -98,7 +126,10 @@ var RedisTtlCache = class {
|
|
|
98
126
|
* @returns {Promise<void>} A promise that resolves when the value is enqueued
|
|
99
127
|
*/
|
|
100
128
|
async enqueueRecord(queueName, value) {
|
|
101
|
-
await this.client.lPush(
|
|
129
|
+
await this.client.lPush(
|
|
130
|
+
queueName,
|
|
131
|
+
this.encryptValue(safeStringify(value))
|
|
132
|
+
);
|
|
102
133
|
}
|
|
103
134
|
/**
|
|
104
135
|
* Adds multiple values to the left end of a Redis list in a single transaction.
|
|
@@ -111,7 +142,10 @@ var RedisTtlCache = class {
|
|
|
111
142
|
async enqueueBatchRecords(queueName, values) {
|
|
112
143
|
const multiCommand = this.client.multi();
|
|
113
144
|
for (const value of values) {
|
|
114
|
-
multiCommand.lPush(
|
|
145
|
+
multiCommand.lPush(
|
|
146
|
+
queueName,
|
|
147
|
+
this.encryptValue(safeStringify(value))
|
|
148
|
+
);
|
|
115
149
|
}
|
|
116
150
|
await multiCommand.exec();
|
|
117
151
|
}
|
|
@@ -150,7 +184,7 @@ var RedisTtlCache = class {
|
|
|
150
184
|
if (value === null) {
|
|
151
185
|
throw new Error(`Queue is empty: ${queueName}`);
|
|
152
186
|
}
|
|
153
|
-
return safeParse(value);
|
|
187
|
+
return safeParse(this.decryptValue(value));
|
|
154
188
|
}
|
|
155
189
|
/**
|
|
156
190
|
* Removes and returns multiple elements from the right end of a Redis list.
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} hostingOptions - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(value) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, safeStringify(value), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, safeStringify(value), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(queueName, safeStringify(value));\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(queueName, safeStringify(value));\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(value) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(queueName: string, pageSize: number): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":";AAAA,SAAS,WAAW,qBAAqB;AAEzC;AAAA,EACE;AAAA,OAIK;AACP,SAAS,oBAAwC;AAkB1C,IAAM,gBAAN,MAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY7C,YACU,iBACA,wBACR,SACA,kBACA;AAJQ;AACA;AAIR,SAAK,mBAAmB,yBAAyB,gBAAgB;AACjE,SAAK,SAAS,aAAa,OAAO;AAClC,QAAI,KAAK,iBAAiB,QAAQ,SAAS;AACzC,WAAK,OAAO,GAAG,SAAS,CAAC,QAAQ,KAAK,uBAAuB,MAAM,GAAG,CAAC;AACvE,WAAK,OAAO,QAAQ,EAAE,MAAM,KAAK,uBAAuB,KAAK;AAAA,IAC/D;AAAA,EACF;AAAA,EAvBQ;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCA,WAAc,OAAgC;AACpD,QAAI,SAAS,MAAM;AACjB,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,aAAO,MAAM,IAAI,CAAC,MAAM,KAAK,WAAc,CAAC,CAAC;AAAA,IAC/C;AAEA,QAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,aAAO,MAAM,OAAO;AAAA,IACtB;AAEA,YAAQ,OAAO,OAAO;AAAA,MACpB,KAAK;AAAA,MACL,KAAK;AACH,eAAO,UAAU,KAAK;AAAA,MACxB,KAAK;AACH,eAAO;AAAA,IACX;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,UAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA,kBAAkB,KAAK;AAAA,EACzB,GAAqC;AACnC,QAAI,KAAK,iBAAiB,QAAQ,SAAS;AACzC,WAAK,uBAAuB,KAAK,8BAA8B,GAAG,EAAE;AAAA,IACtE;AACA,UAAM,KAAK,OAAO,IAAI,KAAK,cAAc,KAAK,GAAG;AAAA,MAC/C,IAAI;AAAA,IACN,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,gBAAmB,cAAkD;AACzE,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,eAAW,EAAE,KAAK,OAAO,gBAAgB,KAAK,cAAc;AAC1D,mBAAa,IAAI,KAAK,cAAc,KAAK,GAAG;AAAA,QAC1C,IAAI,mBAAmB,KAAK;AAAA,MAC9B,CAAC;AAAA,IACH;AACA,UAAM,aAAa,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cAAiB,WAAmB,OAAyB;AACjE,UAAM,KAAK,OAAO,MAAM,WAAW,cAAc,KAAK,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,oBAAuB,WAAmB,QAA4B;AAC1E,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,eAAW,SAAS,QAAQ;AAC1B,mBAAa,MAAM,WAAW,cAAc,KAAK,CAAC;AAAA,IACpD;AACA,UAAM,aAAa,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aAAa,gBAAuC;AACxD,UAAM,KAAK,OAAO,IAAI,cAAc;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,iBAA0C;AACjE,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,eAAW,OAAO,iBAAiB;AACjC,mBAAa,IAAI,GAAG;AAAA,IACtB;AACA,UAAM,aAAa,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cAAiB,WAA+B;AACpD,UAAM,QAAQ,MAAM,KAAK,OAAO,KAAK,SAAS;AAC9C,QAAI,UAAU,MAAM;AAClB,YAAM,IAAI,MAAM,mBAAmB,SAAS,EAAE;AAAA,IAChD;AACA,WAAO,UAAU,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,oBACJ,WACA,UACc;AACd,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,aAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,mBAAa,KAAK,SAAS;AAAA,IAC7B;AACA,UAAM,SAAS,MAAM,aAAa,KAAK;AACvC,WAAO,OACJ;AAAA,MAAI,CAAC,UACJ,KAAK,WAAc,KAAwC;AAAA,IAC7D,EACC,OAAO,OAAO;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,WAAc,gBAAoD;AACtE,UAAM,CAAC,OAAO,GAAG,IAAI,MAAM,KAAK,OAC7B,MAAM,EACN,IAAI,cAAc,EAClB,IAAI,cAAc,EAClB,KAAK;AACR,QAAI,UAAU,MAAM;AAClB,YAAM,IAAI,MAAM,6BAA6B,cAAc,EAAE;AAAA,IAC/D;AAEA,WAAO;AAAA,MACL,KAAK;AAAA,MACL,OAAO,KAAK,WAAc,KAAwC;AAAA,MAClE,iBACE,KAAK,WAAmB,GAAsC,IAAI;AAAA,IACtE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,iBACJ,yBAC8B;AAC9B,UAAM,OAAO,MAAM,QAAQ,uBAAuB,IAC9C,0BACA,MAAM,KAAK,OAAO,KAAK,0BAA0B,GAAG;AACxD,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,eAAW,OAAO,MAAM;AACtB,mBAAa,IAAI,GAAG;AACpB,mBAAa,IAAI,GAAG;AAAA,IACtB;AACA,UAAM,SAAS,MAAM,aAAa,KAAK;AACvC,WAAO,OAAO,OAA4B,CAAC,KAAK,OAAO,UAAU;AAC/D,UAAI,QAAQ,MAAM,GAAG;AACnB,cAAM,aAAa,KAAK;AAAA,UACtB;AAAA,QACF;AACA,cAAM,MAAM,KAAK;AAAA,UACf,OAAO,QAAQ,CAAC;AAAA,QAClB;AACA,YAAI,cAAc,KAAK;AACrB,cAAI,KAAK;AAAA,YACP,KAAK,KAAK,QAAQ,CAAC;AAAA,YACnB,OAAO;AAAA,YACP,iBAAiB,MAAM;AAAA,UACzB,CAAC;AAAA,QACH;AAAA,MACF;AACA,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,gBAA2C;AACxD,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,iBAAiB,GAAG;AACxD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,WAAW,gBAA0C;AACzD,UAAM,SAAS,MAAM,KAAK,OAAO,OAAO,cAAc;AACtD,WAAO,WAAW;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,iBACJ,yBACoB;AACpB,UAAM,OAAO,MAAM,QAAQ,uBAAuB,IAC9C,0BACA,MAAM,KAAK,OAAO,KAAK,0BAA0B,GAAG;AACxD,UAAM,eAAe,KAAK,OAAO,MAAM;AACvC,eAAW,OAAO,MAAM;AACtB,mBAAa,OAAO,GAAG;AAAA,IACzB;AACA,UAAM,UAAU,MAAM,aAAa,KAAK;AACxC,WAAO,QAAQ,IAAI,CAAC,WAAY,WAAiC,CAAC;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,gBAAmB,WAA+B;AACtD,UAAM,QAAQ,MAAM,KAAK,OAAO,OAAO,WAAW,GAAG,CAAC;AACtD,WAAO,KAAK,WAAc,MAAM,CAAC,CAAC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,iBAAoB,WAAmB,UAAgC;AAC3E,UAAM,SAAS,MAAM,KAAK,OAAO,OAAO,WAAW,GAAG,WAAW,CAAC;AAClE,WAAO,OAAO,IAAI,CAAC,UAAU,KAAK,WAAc,KAAK,CAAC,EAAE,OAAO,OAAO;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAA4B;AAChC,UAAM,KAAK,OAAO,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAA6B;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAgC;AAC9B,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport {\n getCurrentTenantId,\n type FieldEncryptor\n} from '@forklaunch/core/persistence';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n/**\n * Options for configuring encryption on the Redis cache.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface RedisCacheEncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting cache values. */\n encryptor: FieldEncryptor;\n /** Set to true to disable encryption. Defaults to false (encryption enabled). */\n disabled?: boolean;\n}\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n *\n * Encryption is enabled by default when an encryptor is provided. Values are encrypted\n * before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n private encryptor?: FieldEncryptor;\n private encryptionDisabled: boolean;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} options - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n * @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions,\n encryption: RedisCacheEncryptionOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n this.encryptor = encryption.encryptor;\n this.encryptionDisabled = encryption.disabled ?? false;\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers\n // ---------------------------------------------------------------------------\n\n private encryptValue(serialized: string): string {\n if (!this.encryptor || this.encryptionDisabled) return serialized;\n return this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;\n }\n\n private decryptValue(value: string): string {\n if (!this.encryptor || this.encryptionDisabled) return value;\n if (!isEncrypted(value)) return value;\n try {\n return this.encryptor.decrypt(value, getCurrentTenantId()) ?? value;\n } catch {\n return value;\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(this.decryptValue(String(value))) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(\n queueName,\n this.encryptValue(safeStringify(value))\n );\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(\n queueName,\n this.encryptValue(safeStringify(value))\n );\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(this.decryptValue(value)) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forklaunch/infrastructure-redis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Redis infrastructure for ForkLaunch components.",
|
|
5
5
|
"homepage": "https://github.com/forklaunch/forklaunch-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"redis": "^5.11.0",
|
|
32
|
-
"@forklaunch/common": "1.2.
|
|
33
|
-
"@forklaunch/core": "1.3.
|
|
32
|
+
"@forklaunch/common": "1.2.6",
|
|
33
|
+
"@forklaunch/core": "1.3.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^10.0.1",
|