@forklaunch/infrastructure-s3 1.2.9 → 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/s3.ts +71 -5
- package/lib/index.d.mts +31 -3
- package/lib/index.d.ts +31 -3
- package/lib/index.js +46 -5
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +48 -5
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -14,9 +14,30 @@ import {
|
|
|
14
14
|
OpenTelemetryCollector,
|
|
15
15
|
TelemetryOptions
|
|
16
16
|
} from '@forklaunch/core/http';
|
|
17
|
+
import {
|
|
18
|
+
getCurrentTenantId,
|
|
19
|
+
type FieldEncryptor
|
|
20
|
+
} from '@forklaunch/core/persistence';
|
|
17
21
|
import { ObjectStore } from '@forklaunch/core/objectstore';
|
|
18
22
|
import { Readable } from 'stream';
|
|
19
23
|
|
|
24
|
+
const ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;
|
|
25
|
+
|
|
26
|
+
function isEncrypted(value: string): boolean {
|
|
27
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for configuring encryption on the S3 object store.
|
|
32
|
+
* Required — every consumer must explicitly configure encryption.
|
|
33
|
+
*/
|
|
34
|
+
export interface S3EncryptionOptions {
|
|
35
|
+
/** The FieldEncryptor instance to use for encrypting object bodies. */
|
|
36
|
+
encryptor: FieldEncryptor;
|
|
37
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
/**
|
|
21
42
|
* Options for configuring the S3ObjectStore.
|
|
22
43
|
*
|
|
@@ -39,6 +60,10 @@ interface S3ObjectStoreOptions {
|
|
|
39
60
|
* S3-backed implementation of the ObjectStore interface.
|
|
40
61
|
* Provides methods for storing, retrieving, streaming, and deleting objects in S3.
|
|
41
62
|
*
|
|
63
|
+
* Encryption is enabled by default when an encryptor is provided. Object bodies
|
|
64
|
+
* are encrypted before upload and decrypted after download using AES-256-GCM
|
|
65
|
+
* with per-tenant key derivation.
|
|
66
|
+
*
|
|
42
67
|
* @example
|
|
43
68
|
* const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
|
|
44
69
|
* await store.putObject({ key: 'user-1', name: 'Alice' });
|
|
@@ -48,26 +73,60 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
48
73
|
private s3: S3Client;
|
|
49
74
|
private bucket: string;
|
|
50
75
|
private initialized: boolean;
|
|
76
|
+
private encryptor?: FieldEncryptor;
|
|
77
|
+
private encryptionDisabled: boolean;
|
|
51
78
|
|
|
52
79
|
/**
|
|
53
80
|
* Creates a new S3ObjectStore instance.
|
|
54
81
|
* @param openTelemetryCollector - Collector for OpenTelemetry metrics.
|
|
55
82
|
* @param options - S3 configuration options.
|
|
56
83
|
* @param telemetryOptions - Telemetry configuration options.
|
|
84
|
+
* @param encryption - Encryption configuration (enabled by default when encryptor provided).
|
|
57
85
|
*
|
|
58
86
|
* @example
|
|
59
|
-
* const store = new S3ObjectStore(
|
|
87
|
+
* const store = new S3ObjectStore(
|
|
88
|
+
* otelCollector,
|
|
89
|
+
* { bucket: 'my-bucket' },
|
|
90
|
+
* telemetryOptions,
|
|
91
|
+
* { encryptor }
|
|
92
|
+
* );
|
|
60
93
|
*/
|
|
61
94
|
constructor(
|
|
62
95
|
private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,
|
|
63
96
|
options: S3ObjectStoreOptions,
|
|
64
|
-
private telemetryOptions: TelemetryOptions
|
|
97
|
+
private telemetryOptions: TelemetryOptions,
|
|
98
|
+
encryption: S3EncryptionOptions
|
|
65
99
|
) {
|
|
66
100
|
this.s3 = options.client || new S3Client(options.clientConfig || {});
|
|
67
101
|
this.bucket = options.bucket;
|
|
68
102
|
this.initialized = false;
|
|
103
|
+
this.encryptor = encryption.encryptor;
|
|
104
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
69
105
|
}
|
|
70
106
|
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Encryption helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
private encryptBody(body: string): string {
|
|
112
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
113
|
+
return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private decryptBody(body: string): string {
|
|
117
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
118
|
+
if (!isEncrypted(body)) return body;
|
|
119
|
+
try {
|
|
120
|
+
return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;
|
|
121
|
+
} catch {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Internal
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
71
130
|
private async ensureBucketExists() {
|
|
72
131
|
try {
|
|
73
132
|
await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
@@ -92,10 +151,11 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
92
151
|
}
|
|
93
152
|
|
|
94
153
|
const { key, ...rest } = object;
|
|
154
|
+
const body = this.encryptBody(JSON.stringify(rest));
|
|
95
155
|
const params: PutObjectCommandInput = {
|
|
96
156
|
Bucket: this.bucket,
|
|
97
157
|
Key: key,
|
|
98
|
-
Body:
|
|
158
|
+
Body: body,
|
|
99
159
|
ContentType: 'application/json'
|
|
100
160
|
};
|
|
101
161
|
await this.s3.send(new PutObjectCommand(params));
|
|
@@ -186,7 +246,8 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
186
246
|
throw new Error('S3 did not return a body');
|
|
187
247
|
}
|
|
188
248
|
|
|
189
|
-
|
|
249
|
+
const raw = await resp.Body.transformToString();
|
|
250
|
+
return JSON.parse(this.decryptBody(raw)) as T;
|
|
190
251
|
}
|
|
191
252
|
|
|
192
253
|
/**
|
|
@@ -204,6 +265,8 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
204
265
|
|
|
205
266
|
/**
|
|
206
267
|
* Streams an object download from the S3 bucket.
|
|
268
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
269
|
+
* Use readObject for encrypted objects.
|
|
207
270
|
* @param objectKey - The key of the object to download.
|
|
208
271
|
* @returns A readable stream of the object's contents.
|
|
209
272
|
* @throws If the S3 response does not include a readable stream.
|
|
@@ -228,6 +291,7 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
228
291
|
|
|
229
292
|
/**
|
|
230
293
|
* Streams multiple object downloads from the S3 bucket.
|
|
294
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
231
295
|
* @param objectKeys - The keys of the objects to download.
|
|
232
296
|
* @returns An array of readable streams.
|
|
233
297
|
*
|
|
@@ -236,7 +300,9 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
236
300
|
* streams[0].pipe(fs.createWriteStream('user-1.json'));
|
|
237
301
|
*/
|
|
238
302
|
async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {
|
|
239
|
-
return Promise.all(
|
|
303
|
+
return Promise.all(
|
|
304
|
+
objectKeys.map((key) => this.streamDownloadObject(key))
|
|
305
|
+
);
|
|
240
306
|
}
|
|
241
307
|
|
|
242
308
|
/**
|
package/lib/index.d.mts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
2
|
import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
|
|
3
|
+
import { FieldEncryptor } from '@forklaunch/core/persistence';
|
|
3
4
|
import { ObjectStore } from '@forklaunch/core/objectstore';
|
|
4
5
|
import { Readable } from 'stream';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Options for configuring encryption on the S3 object store.
|
|
9
|
+
* Required — every consumer must explicitly configure encryption.
|
|
10
|
+
*/
|
|
11
|
+
interface S3EncryptionOptions {
|
|
12
|
+
/** The FieldEncryptor instance to use for encrypting object bodies. */
|
|
13
|
+
encryptor: FieldEncryptor;
|
|
14
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Options for configuring the S3ObjectStore.
|
|
8
19
|
*
|
|
@@ -24,6 +35,10 @@ interface S3ObjectStoreOptions {
|
|
|
24
35
|
* S3-backed implementation of the ObjectStore interface.
|
|
25
36
|
* Provides methods for storing, retrieving, streaming, and deleting objects in S3.
|
|
26
37
|
*
|
|
38
|
+
* Encryption is enabled by default when an encryptor is provided. Object bodies
|
|
39
|
+
* are encrypted before upload and decrypted after download using AES-256-GCM
|
|
40
|
+
* with per-tenant key derivation.
|
|
41
|
+
*
|
|
27
42
|
* @example
|
|
28
43
|
* const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
|
|
29
44
|
* await store.putObject({ key: 'user-1', name: 'Alice' });
|
|
@@ -35,16 +50,26 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
35
50
|
private s3;
|
|
36
51
|
private bucket;
|
|
37
52
|
private initialized;
|
|
53
|
+
private encryptor?;
|
|
54
|
+
private encryptionDisabled;
|
|
38
55
|
/**
|
|
39
56
|
* Creates a new S3ObjectStore instance.
|
|
40
57
|
* @param openTelemetryCollector - Collector for OpenTelemetry metrics.
|
|
41
58
|
* @param options - S3 configuration options.
|
|
42
59
|
* @param telemetryOptions - Telemetry configuration options.
|
|
60
|
+
* @param encryption - Encryption configuration (enabled by default when encryptor provided).
|
|
43
61
|
*
|
|
44
62
|
* @example
|
|
45
|
-
* const store = new S3ObjectStore(
|
|
63
|
+
* const store = new S3ObjectStore(
|
|
64
|
+
* otelCollector,
|
|
65
|
+
* { bucket: 'my-bucket' },
|
|
66
|
+
* telemetryOptions,
|
|
67
|
+
* { encryptor }
|
|
68
|
+
* );
|
|
46
69
|
*/
|
|
47
|
-
constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions);
|
|
70
|
+
constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions, encryption: S3EncryptionOptions);
|
|
71
|
+
private encryptBody;
|
|
72
|
+
private decryptBody;
|
|
48
73
|
private ensureBucketExists;
|
|
49
74
|
/**
|
|
50
75
|
* Stores an object in the S3 bucket.
|
|
@@ -127,6 +152,8 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
127
152
|
readBatchObjects<T>(objectKeys: string[]): Promise<T[]>;
|
|
128
153
|
/**
|
|
129
154
|
* Streams an object download from the S3 bucket.
|
|
155
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
156
|
+
* Use readObject for encrypted objects.
|
|
130
157
|
* @param objectKey - The key of the object to download.
|
|
131
158
|
* @returns A readable stream of the object's contents.
|
|
132
159
|
* @throws If the S3 response does not include a readable stream.
|
|
@@ -138,6 +165,7 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
138
165
|
streamDownloadObject(objectKey: string): Promise<Readable>;
|
|
139
166
|
/**
|
|
140
167
|
* Streams multiple object downloads from the S3 bucket.
|
|
168
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
141
169
|
* @param objectKeys - The keys of the objects to download.
|
|
142
170
|
* @returns An array of readable streams.
|
|
143
171
|
*
|
|
@@ -156,4 +184,4 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
156
184
|
getClient(): S3Client;
|
|
157
185
|
}
|
|
158
186
|
|
|
159
|
-
export { S3ObjectStore };
|
|
187
|
+
export { type S3EncryptionOptions, S3ObjectStore };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
2
|
import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
|
|
3
|
+
import { FieldEncryptor } from '@forklaunch/core/persistence';
|
|
3
4
|
import { ObjectStore } from '@forklaunch/core/objectstore';
|
|
4
5
|
import { Readable } from 'stream';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Options for configuring encryption on the S3 object store.
|
|
9
|
+
* Required — every consumer must explicitly configure encryption.
|
|
10
|
+
*/
|
|
11
|
+
interface S3EncryptionOptions {
|
|
12
|
+
/** The FieldEncryptor instance to use for encrypting object bodies. */
|
|
13
|
+
encryptor: FieldEncryptor;
|
|
14
|
+
/** Set to true to disable encryption. Defaults to false (encryption enabled). */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Options for configuring the S3ObjectStore.
|
|
8
19
|
*
|
|
@@ -24,6 +35,10 @@ interface S3ObjectStoreOptions {
|
|
|
24
35
|
* S3-backed implementation of the ObjectStore interface.
|
|
25
36
|
* Provides methods for storing, retrieving, streaming, and deleting objects in S3.
|
|
26
37
|
*
|
|
38
|
+
* Encryption is enabled by default when an encryptor is provided. Object bodies
|
|
39
|
+
* are encrypted before upload and decrypted after download using AES-256-GCM
|
|
40
|
+
* with per-tenant key derivation.
|
|
41
|
+
*
|
|
27
42
|
* @example
|
|
28
43
|
* const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
|
|
29
44
|
* await store.putObject({ key: 'user-1', name: 'Alice' });
|
|
@@ -35,16 +50,26 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
35
50
|
private s3;
|
|
36
51
|
private bucket;
|
|
37
52
|
private initialized;
|
|
53
|
+
private encryptor?;
|
|
54
|
+
private encryptionDisabled;
|
|
38
55
|
/**
|
|
39
56
|
* Creates a new S3ObjectStore instance.
|
|
40
57
|
* @param openTelemetryCollector - Collector for OpenTelemetry metrics.
|
|
41
58
|
* @param options - S3 configuration options.
|
|
42
59
|
* @param telemetryOptions - Telemetry configuration options.
|
|
60
|
+
* @param encryption - Encryption configuration (enabled by default when encryptor provided).
|
|
43
61
|
*
|
|
44
62
|
* @example
|
|
45
|
-
* const store = new S3ObjectStore(
|
|
63
|
+
* const store = new S3ObjectStore(
|
|
64
|
+
* otelCollector,
|
|
65
|
+
* { bucket: 'my-bucket' },
|
|
66
|
+
* telemetryOptions,
|
|
67
|
+
* { encryptor }
|
|
68
|
+
* );
|
|
46
69
|
*/
|
|
47
|
-
constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions);
|
|
70
|
+
constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions, encryption: S3EncryptionOptions);
|
|
71
|
+
private encryptBody;
|
|
72
|
+
private decryptBody;
|
|
48
73
|
private ensureBucketExists;
|
|
49
74
|
/**
|
|
50
75
|
* Stores an object in the S3 bucket.
|
|
@@ -127,6 +152,8 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
127
152
|
readBatchObjects<T>(objectKeys: string[]): Promise<T[]>;
|
|
128
153
|
/**
|
|
129
154
|
* Streams an object download from the S3 bucket.
|
|
155
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
156
|
+
* Use readObject for encrypted objects.
|
|
130
157
|
* @param objectKey - The key of the object to download.
|
|
131
158
|
* @returns A readable stream of the object's contents.
|
|
132
159
|
* @throws If the S3 response does not include a readable stream.
|
|
@@ -138,6 +165,7 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
138
165
|
streamDownloadObject(objectKey: string): Promise<Readable>;
|
|
139
166
|
/**
|
|
140
167
|
* Streams multiple object downloads from the S3 bucket.
|
|
168
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
141
169
|
* @param objectKeys - The keys of the objects to download.
|
|
142
170
|
* @returns An array of readable streams.
|
|
143
171
|
*
|
|
@@ -156,4 +184,4 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
|
|
|
156
184
|
getClient(): S3Client;
|
|
157
185
|
}
|
|
158
186
|
|
|
159
|
-
export { S3ObjectStore };
|
|
187
|
+
export { type S3EncryptionOptions, S3ObjectStore };
|
package/lib/index.js
CHANGED
|
@@ -24,27 +24,61 @@ __export(index_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
27
|
+
var import_persistence = require("@forklaunch/core/persistence");
|
|
27
28
|
var import_stream = require("stream");
|
|
29
|
+
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
30
|
+
function isEncrypted(value) {
|
|
31
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
32
|
+
}
|
|
28
33
|
var S3ObjectStore = class {
|
|
29
34
|
/**
|
|
30
35
|
* Creates a new S3ObjectStore instance.
|
|
31
36
|
* @param openTelemetryCollector - Collector for OpenTelemetry metrics.
|
|
32
37
|
* @param options - S3 configuration options.
|
|
33
38
|
* @param telemetryOptions - Telemetry configuration options.
|
|
39
|
+
* @param encryption - Encryption configuration (enabled by default when encryptor provided).
|
|
34
40
|
*
|
|
35
41
|
* @example
|
|
36
|
-
* const store = new S3ObjectStore(
|
|
42
|
+
* const store = new S3ObjectStore(
|
|
43
|
+
* otelCollector,
|
|
44
|
+
* { bucket: 'my-bucket' },
|
|
45
|
+
* telemetryOptions,
|
|
46
|
+
* { encryptor }
|
|
47
|
+
* );
|
|
37
48
|
*/
|
|
38
|
-
constructor(openTelemetryCollector, options, telemetryOptions) {
|
|
49
|
+
constructor(openTelemetryCollector, options, telemetryOptions, encryption) {
|
|
39
50
|
this.openTelemetryCollector = openTelemetryCollector;
|
|
40
51
|
this.telemetryOptions = telemetryOptions;
|
|
41
52
|
this.s3 = options.client || new import_client_s3.S3Client(options.clientConfig || {});
|
|
42
53
|
this.bucket = options.bucket;
|
|
43
54
|
this.initialized = false;
|
|
55
|
+
this.encryptor = encryption.encryptor;
|
|
56
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
44
57
|
}
|
|
45
58
|
s3;
|
|
46
59
|
bucket;
|
|
47
60
|
initialized;
|
|
61
|
+
encryptor;
|
|
62
|
+
encryptionDisabled;
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Encryption helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
encryptBody(body) {
|
|
67
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
68
|
+
return this.encryptor.encrypt(body, (0, import_persistence.getCurrentTenantId)()) ?? body;
|
|
69
|
+
}
|
|
70
|
+
decryptBody(body) {
|
|
71
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
72
|
+
if (!isEncrypted(body)) return body;
|
|
73
|
+
try {
|
|
74
|
+
return this.encryptor.decrypt(body, (0, import_persistence.getCurrentTenantId)()) ?? body;
|
|
75
|
+
} catch {
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Internal
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
48
82
|
async ensureBucketExists() {
|
|
49
83
|
try {
|
|
50
84
|
await this.s3.send(new import_client_s3.HeadBucketCommand({ Bucket: this.bucket }));
|
|
@@ -66,10 +100,11 @@ var S3ObjectStore = class {
|
|
|
66
100
|
await this.ensureBucketExists();
|
|
67
101
|
}
|
|
68
102
|
const { key, ...rest } = object;
|
|
103
|
+
const body = this.encryptBody(JSON.stringify(rest));
|
|
69
104
|
const params = {
|
|
70
105
|
Bucket: this.bucket,
|
|
71
106
|
Key: key,
|
|
72
|
-
Body:
|
|
107
|
+
Body: body,
|
|
73
108
|
ContentType: "application/json"
|
|
74
109
|
};
|
|
75
110
|
await this.s3.send(new import_client_s3.PutObjectCommand(params));
|
|
@@ -150,7 +185,8 @@ var S3ObjectStore = class {
|
|
|
150
185
|
if (!resp.Body) {
|
|
151
186
|
throw new Error("S3 did not return a body");
|
|
152
187
|
}
|
|
153
|
-
|
|
188
|
+
const raw = await resp.Body.transformToString();
|
|
189
|
+
return JSON.parse(this.decryptBody(raw));
|
|
154
190
|
}
|
|
155
191
|
/**
|
|
156
192
|
* Reads multiple objects from the S3 bucket.
|
|
@@ -166,6 +202,8 @@ var S3ObjectStore = class {
|
|
|
166
202
|
}
|
|
167
203
|
/**
|
|
168
204
|
* Streams an object download from the S3 bucket.
|
|
205
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
206
|
+
* Use readObject for encrypted objects.
|
|
169
207
|
* @param objectKey - The key of the object to download.
|
|
170
208
|
* @returns A readable stream of the object's contents.
|
|
171
209
|
* @throws If the S3 response does not include a readable stream.
|
|
@@ -188,6 +226,7 @@ var S3ObjectStore = class {
|
|
|
188
226
|
}
|
|
189
227
|
/**
|
|
190
228
|
* Streams multiple object downloads from the S3 bucket.
|
|
229
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
191
230
|
* @param objectKeys - The keys of the objects to download.
|
|
192
231
|
* @returns An array of readable streams.
|
|
193
232
|
*
|
|
@@ -196,7 +235,9 @@ var S3ObjectStore = class {
|
|
|
196
235
|
* streams[0].pipe(fs.createWriteStream('user-1.json'));
|
|
197
236
|
*/
|
|
198
237
|
async streamDownloadBatchObjects(objectKeys) {
|
|
199
|
-
return Promise.all(
|
|
238
|
+
return Promise.all(
|
|
239
|
+
objectKeys.map((key) => this.streamDownloadObject(key))
|
|
240
|
+
);
|
|
200
241
|
}
|
|
201
242
|
/**
|
|
202
243
|
* Gets the underlying S3 client instance.
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import {\n CreateBucketCommand,\n DeleteObjectCommand,\n DeleteObjectsCommand,\n DeleteObjectsCommandInput,\n GetObjectCommand,\n HeadBucketCommand,\n PutObjectCommand,\n PutObjectCommandInput,\n S3Client\n} from '@aws-sdk/client-s3';\nimport {\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport { Readable } from 'stream';\n\n/**\n * Options for configuring the S3ObjectStore.\n *\n * @example\n * const options: S3ObjectStoreOptions = {\n * bucket: 'my-bucket',\n * clientConfig: { region: 'us-west-2' }\n * };\n */\ninterface S3ObjectStoreOptions {\n /** The S3 bucket name. */\n bucket: string;\n /** Optional existing S3 client instance. */\n client?: S3Client;\n /** Optional configuration for creating a new S3 client. */\n clientConfig?: ConstructorParameters<typeof S3Client>[0];\n}\n\n/**\n * S3-backed implementation of the ObjectStore interface.\n * Provides methods for storing, retrieving, streaming, and deleting objects in S3.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n * const user = await store.readObject<{ name: string }>('user-1');\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n\n /**\n * Creates a new S3ObjectStore instance.\n * @param openTelemetryCollector - Collector for OpenTelemetry metrics.\n * @param options - S3 configuration options.\n * @param telemetryOptions - Telemetry configuration options.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n */\n constructor(\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: S3ObjectStoreOptions,\n private telemetryOptions: TelemetryOptions\n ) {\n this.s3 = options.client || new S3Client(options.clientConfig || {});\n this.bucket = options.bucket;\n this.initialized = false;\n }\n\n private async ensureBucketExists() {\n try {\n await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));\n } catch {\n await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket }));\n }\n\n this.initialized = true;\n }\n\n /**\n * Stores an object in the S3 bucket.\n * @template T - The type of the object being stored.\n * @param object - The object to store. Must include a `key` property.\n *\n * @example\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n */\n async putObject<T>(object: T & { key: string }): Promise<void> {\n if (!this.initialized) {\n await this.ensureBucketExists();\n }\n\n const { key, ...rest } = object;\n const params: PutObjectCommandInput = {\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(rest),\n ContentType: 'application/json'\n };\n await this.s3.send(new PutObjectCommand(params));\n }\n\n /**\n * Stores multiple objects in the S3 bucket.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to store. Each must include a `key` property.\n *\n * @example\n * await store.putBatchObjects([\n * { key: 'user-1', name: 'Alice' },\n * { key: 'user-2', name: 'Bob' }\n * ]);\n */\n async putBatchObjects<T>(objects: (T & { key: string })[]): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj)));\n }\n\n /**\n * Streams an object upload to the S3 bucket.\n * For compatibility; uses putObject internally.\n * @template T - The type of the object being stored.\n * @param object - The object to stream-upload. Must include a `key` property.\n */\n async streamUploadObject<T>(object: T & { key: string }): Promise<void> {\n await this.putObject(object);\n }\n\n /**\n * Streams multiple object uploads to the S3 bucket.\n * For compatibility; uses putBatchObjects internally.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to stream-upload. Each must include a `key` property.\n */\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[]\n ): Promise<void> {\n await this.putBatchObjects(objects);\n }\n\n /**\n * Deletes an object from the S3 bucket.\n * @param objectKey - The key of the object to delete.\n *\n * @example\n * await store.deleteObject('user-1');\n */\n async deleteObject(objectKey: string): Promise<void> {\n await this.s3.send(\n new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n }\n\n /**\n * Deletes multiple objects from the S3 bucket.\n * @param objectKeys - The keys of the objects to delete.\n *\n * @example\n * await store.deleteBatchObjects(['user-1', 'user-2']);\n */\n async deleteBatchObjects(objectKeys: string[]): Promise<void> {\n const params: DeleteObjectsCommandInput = {\n Bucket: this.bucket,\n Delete: {\n Objects: objectKeys.map((Key) => ({ Key }))\n }\n };\n await this.s3.send(new DeleteObjectsCommand(params));\n }\n\n /**\n * Reads an object from the S3 bucket.\n * @template T - The expected type of the object.\n * @param objectKey - The key of the object to read.\n * @returns The parsed object.\n *\n * @example\n * const user = await store.readObject<{ name: string }>('user-1');\n */\n async readObject<T>(objectKey: string): Promise<T> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n\n if (!resp.Body) {\n throw new Error('S3 did not return a body');\n }\n\n return JSON.parse(await resp.Body.transformToString()) as T;\n }\n\n /**\n * Reads multiple objects from the S3 bucket.\n * @template T - The expected type of the objects.\n * @param objectKeys - The keys of the objects to read.\n * @returns An array of parsed objects.\n *\n * @example\n * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);\n */\n async readBatchObjects<T>(objectKeys: string[]): Promise<T[]> {\n return Promise.all(objectKeys.map((key) => this.readObject<T>(key)));\n }\n\n /**\n * Streams an object download from the S3 bucket.\n * @param objectKey - The key of the object to download.\n * @returns A readable stream of the object's contents.\n * @throws If the S3 response does not include a readable stream.\n *\n * @example\n * const stream = await store.streamDownloadObject('user-1');\n * stream.pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadObject(objectKey: string): Promise<Readable> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n const webStream = resp.Body?.transformToWebStream();\n if (!webStream) {\n throw new Error('S3 did not return a stream');\n }\n\n return Readable.fromWeb(\n webStream as Parameters<typeof Readable.fromWeb>[0]\n );\n }\n\n /**\n * Streams multiple object downloads from the S3 bucket.\n * @param objectKeys - The keys of the objects to download.\n * @returns An array of readable streams.\n *\n * @example\n * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);\n * streams[0].pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));\n }\n\n /**\n * Gets the underlying S3 client instance.\n * @returns The S3Client instance used by this store.\n *\n * @example\n * const s3Client = store.getClient();\n */\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import {\n CreateBucketCommand,\n DeleteObjectCommand,\n DeleteObjectsCommand,\n DeleteObjectsCommandInput,\n GetObjectCommand,\n HeadBucketCommand,\n PutObjectCommand,\n PutObjectCommandInput,\n S3Client\n} from '@aws-sdk/client-s3';\nimport {\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport {\n getCurrentTenantId,\n type FieldEncryptor\n} from '@forklaunch/core/persistence';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport { Readable } from 'stream';\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 S3 object store.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface S3EncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting object bodies. */\n encryptor: FieldEncryptor;\n /** Set to true to disable encryption. Defaults to false (encryption enabled). */\n disabled?: boolean;\n}\n\n/**\n * Options for configuring the S3ObjectStore.\n *\n * @example\n * const options: S3ObjectStoreOptions = {\n * bucket: 'my-bucket',\n * clientConfig: { region: 'us-west-2' }\n * };\n */\ninterface S3ObjectStoreOptions {\n /** The S3 bucket name. */\n bucket: string;\n /** Optional existing S3 client instance. */\n client?: S3Client;\n /** Optional configuration for creating a new S3 client. */\n clientConfig?: ConstructorParameters<typeof S3Client>[0];\n}\n\n/**\n * S3-backed implementation of the ObjectStore interface.\n * Provides methods for storing, retrieving, streaming, and deleting objects in S3.\n *\n * Encryption is enabled by default when an encryptor is provided. Object bodies\n * are encrypted before upload and decrypted after download using AES-256-GCM\n * with per-tenant key derivation.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n * const user = await store.readObject<{ name: string }>('user-1');\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n private encryptor?: FieldEncryptor;\n private encryptionDisabled: boolean;\n\n /**\n * Creates a new S3ObjectStore instance.\n * @param openTelemetryCollector - Collector for OpenTelemetry metrics.\n * @param options - S3 configuration options.\n * @param telemetryOptions - Telemetry configuration options.\n * @param encryption - Encryption configuration (enabled by default when encryptor provided).\n *\n * @example\n * const store = new S3ObjectStore(\n * otelCollector,\n * { bucket: 'my-bucket' },\n * telemetryOptions,\n * { encryptor }\n * );\n */\n constructor(\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: S3ObjectStoreOptions,\n private telemetryOptions: TelemetryOptions,\n encryption: S3EncryptionOptions\n ) {\n this.s3 = options.client || new S3Client(options.clientConfig || {});\n this.bucket = options.bucket;\n this.initialized = false;\n this.encryptor = encryption.encryptor;\n this.encryptionDisabled = encryption.disabled ?? false;\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers\n // ---------------------------------------------------------------------------\n\n private encryptBody(body: string): string {\n if (!this.encryptor || this.encryptionDisabled) return body;\n return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;\n }\n\n private decryptBody(body: string): string {\n if (!this.encryptor || this.encryptionDisabled) return body;\n if (!isEncrypted(body)) return body;\n try {\n return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;\n } catch {\n return body;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------------------\n\n private async ensureBucketExists() {\n try {\n await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));\n } catch {\n await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket }));\n }\n\n this.initialized = true;\n }\n\n /**\n * Stores an object in the S3 bucket.\n * @template T - The type of the object being stored.\n * @param object - The object to store. Must include a `key` property.\n *\n * @example\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n */\n async putObject<T>(object: T & { key: string }): Promise<void> {\n if (!this.initialized) {\n await this.ensureBucketExists();\n }\n\n const { key, ...rest } = object;\n const body = this.encryptBody(JSON.stringify(rest));\n const params: PutObjectCommandInput = {\n Bucket: this.bucket,\n Key: key,\n Body: body,\n ContentType: 'application/json'\n };\n await this.s3.send(new PutObjectCommand(params));\n }\n\n /**\n * Stores multiple objects in the S3 bucket.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to store. Each must include a `key` property.\n *\n * @example\n * await store.putBatchObjects([\n * { key: 'user-1', name: 'Alice' },\n * { key: 'user-2', name: 'Bob' }\n * ]);\n */\n async putBatchObjects<T>(objects: (T & { key: string })[]): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj)));\n }\n\n /**\n * Streams an object upload to the S3 bucket.\n * For compatibility; uses putObject internally.\n * @template T - The type of the object being stored.\n * @param object - The object to stream-upload. Must include a `key` property.\n */\n async streamUploadObject<T>(object: T & { key: string }): Promise<void> {\n await this.putObject(object);\n }\n\n /**\n * Streams multiple object uploads to the S3 bucket.\n * For compatibility; uses putBatchObjects internally.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to stream-upload. Each must include a `key` property.\n */\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[]\n ): Promise<void> {\n await this.putBatchObjects(objects);\n }\n\n /**\n * Deletes an object from the S3 bucket.\n * @param objectKey - The key of the object to delete.\n *\n * @example\n * await store.deleteObject('user-1');\n */\n async deleteObject(objectKey: string): Promise<void> {\n await this.s3.send(\n new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n }\n\n /**\n * Deletes multiple objects from the S3 bucket.\n * @param objectKeys - The keys of the objects to delete.\n *\n * @example\n * await store.deleteBatchObjects(['user-1', 'user-2']);\n */\n async deleteBatchObjects(objectKeys: string[]): Promise<void> {\n const params: DeleteObjectsCommandInput = {\n Bucket: this.bucket,\n Delete: {\n Objects: objectKeys.map((Key) => ({ Key }))\n }\n };\n await this.s3.send(new DeleteObjectsCommand(params));\n }\n\n /**\n * Reads an object from the S3 bucket.\n * @template T - The expected type of the object.\n * @param objectKey - The key of the object to read.\n * @returns The parsed object.\n *\n * @example\n * const user = await store.readObject<{ name: string }>('user-1');\n */\n async readObject<T>(objectKey: string): Promise<T> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n\n if (!resp.Body) {\n throw new Error('S3 did not return a body');\n }\n\n const raw = await resp.Body.transformToString();\n return JSON.parse(this.decryptBody(raw)) as T;\n }\n\n /**\n * Reads multiple objects from the S3 bucket.\n * @template T - The expected type of the objects.\n * @param objectKeys - The keys of the objects to read.\n * @returns An array of parsed objects.\n *\n * @example\n * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);\n */\n async readBatchObjects<T>(objectKeys: string[]): Promise<T[]> {\n return Promise.all(objectKeys.map((key) => this.readObject<T>(key)));\n }\n\n /**\n * Streams an object download from the S3 bucket.\n * Note: Streaming bypasses application-level encryption/decryption.\n * Use readObject for encrypted objects.\n * @param objectKey - The key of the object to download.\n * @returns A readable stream of the object's contents.\n * @throws If the S3 response does not include a readable stream.\n *\n * @example\n * const stream = await store.streamDownloadObject('user-1');\n * stream.pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadObject(objectKey: string): Promise<Readable> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n const webStream = resp.Body?.transformToWebStream();\n if (!webStream) {\n throw new Error('S3 did not return a stream');\n }\n\n return Readable.fromWeb(\n webStream as Parameters<typeof Readable.fromWeb>[0]\n );\n }\n\n /**\n * Streams multiple object downloads from the S3 bucket.\n * Note: Streaming bypasses application-level encryption/decryption.\n * @param objectKeys - The keys of the objects to download.\n * @returns An array of readable streams.\n *\n * @example\n * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);\n * streams[0].pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(\n objectKeys.map((key) => this.streamDownloadObject(key))\n );\n }\n\n /**\n * Gets the underlying S3 client instance.\n * @returns The S3Client instance used by this store.\n *\n * @example\n * const s3Client = store.getClient();\n */\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":"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","names":[]}
|
package/lib/index.mjs
CHANGED
|
@@ -8,27 +8,63 @@ import {
|
|
|
8
8
|
PutObjectCommand,
|
|
9
9
|
S3Client
|
|
10
10
|
} from "@aws-sdk/client-s3";
|
|
11
|
+
import {
|
|
12
|
+
getCurrentTenantId
|
|
13
|
+
} from "@forklaunch/core/persistence";
|
|
11
14
|
import { Readable } from "stream";
|
|
15
|
+
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
16
|
+
function isEncrypted(value) {
|
|
17
|
+
return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
|
|
18
|
+
}
|
|
12
19
|
var S3ObjectStore = class {
|
|
13
20
|
/**
|
|
14
21
|
* Creates a new S3ObjectStore instance.
|
|
15
22
|
* @param openTelemetryCollector - Collector for OpenTelemetry metrics.
|
|
16
23
|
* @param options - S3 configuration options.
|
|
17
24
|
* @param telemetryOptions - Telemetry configuration options.
|
|
25
|
+
* @param encryption - Encryption configuration (enabled by default when encryptor provided).
|
|
18
26
|
*
|
|
19
27
|
* @example
|
|
20
|
-
* const store = new S3ObjectStore(
|
|
28
|
+
* const store = new S3ObjectStore(
|
|
29
|
+
* otelCollector,
|
|
30
|
+
* { bucket: 'my-bucket' },
|
|
31
|
+
* telemetryOptions,
|
|
32
|
+
* { encryptor }
|
|
33
|
+
* );
|
|
21
34
|
*/
|
|
22
|
-
constructor(openTelemetryCollector, options, telemetryOptions) {
|
|
35
|
+
constructor(openTelemetryCollector, options, telemetryOptions, encryption) {
|
|
23
36
|
this.openTelemetryCollector = openTelemetryCollector;
|
|
24
37
|
this.telemetryOptions = telemetryOptions;
|
|
25
38
|
this.s3 = options.client || new S3Client(options.clientConfig || {});
|
|
26
39
|
this.bucket = options.bucket;
|
|
27
40
|
this.initialized = false;
|
|
41
|
+
this.encryptor = encryption.encryptor;
|
|
42
|
+
this.encryptionDisabled = encryption.disabled ?? false;
|
|
28
43
|
}
|
|
29
44
|
s3;
|
|
30
45
|
bucket;
|
|
31
46
|
initialized;
|
|
47
|
+
encryptor;
|
|
48
|
+
encryptionDisabled;
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Encryption helpers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
encryptBody(body) {
|
|
53
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
54
|
+
return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;
|
|
55
|
+
}
|
|
56
|
+
decryptBody(body) {
|
|
57
|
+
if (!this.encryptor || this.encryptionDisabled) return body;
|
|
58
|
+
if (!isEncrypted(body)) return body;
|
|
59
|
+
try {
|
|
60
|
+
return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;
|
|
61
|
+
} catch {
|
|
62
|
+
return body;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Internal
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
32
68
|
async ensureBucketExists() {
|
|
33
69
|
try {
|
|
34
70
|
await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
@@ -50,10 +86,11 @@ var S3ObjectStore = class {
|
|
|
50
86
|
await this.ensureBucketExists();
|
|
51
87
|
}
|
|
52
88
|
const { key, ...rest } = object;
|
|
89
|
+
const body = this.encryptBody(JSON.stringify(rest));
|
|
53
90
|
const params = {
|
|
54
91
|
Bucket: this.bucket,
|
|
55
92
|
Key: key,
|
|
56
|
-
Body:
|
|
93
|
+
Body: body,
|
|
57
94
|
ContentType: "application/json"
|
|
58
95
|
};
|
|
59
96
|
await this.s3.send(new PutObjectCommand(params));
|
|
@@ -134,7 +171,8 @@ var S3ObjectStore = class {
|
|
|
134
171
|
if (!resp.Body) {
|
|
135
172
|
throw new Error("S3 did not return a body");
|
|
136
173
|
}
|
|
137
|
-
|
|
174
|
+
const raw = await resp.Body.transformToString();
|
|
175
|
+
return JSON.parse(this.decryptBody(raw));
|
|
138
176
|
}
|
|
139
177
|
/**
|
|
140
178
|
* Reads multiple objects from the S3 bucket.
|
|
@@ -150,6 +188,8 @@ var S3ObjectStore = class {
|
|
|
150
188
|
}
|
|
151
189
|
/**
|
|
152
190
|
* Streams an object download from the S3 bucket.
|
|
191
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
192
|
+
* Use readObject for encrypted objects.
|
|
153
193
|
* @param objectKey - The key of the object to download.
|
|
154
194
|
* @returns A readable stream of the object's contents.
|
|
155
195
|
* @throws If the S3 response does not include a readable stream.
|
|
@@ -172,6 +212,7 @@ var S3ObjectStore = class {
|
|
|
172
212
|
}
|
|
173
213
|
/**
|
|
174
214
|
* Streams multiple object downloads from the S3 bucket.
|
|
215
|
+
* Note: Streaming bypasses application-level encryption/decryption.
|
|
175
216
|
* @param objectKeys - The keys of the objects to download.
|
|
176
217
|
* @returns An array of readable streams.
|
|
177
218
|
*
|
|
@@ -180,7 +221,9 @@ var S3ObjectStore = class {
|
|
|
180
221
|
* streams[0].pipe(fs.createWriteStream('user-1.json'));
|
|
181
222
|
*/
|
|
182
223
|
async streamDownloadBatchObjects(objectKeys) {
|
|
183
|
-
return Promise.all(
|
|
224
|
+
return Promise.all(
|
|
225
|
+
objectKeys.map((key) => this.streamDownloadObject(key))
|
|
226
|
+
);
|
|
184
227
|
}
|
|
185
228
|
/**
|
|
186
229
|
* Gets the underlying S3 client instance.
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import {\n CreateBucketCommand,\n DeleteObjectCommand,\n DeleteObjectsCommand,\n DeleteObjectsCommandInput,\n GetObjectCommand,\n HeadBucketCommand,\n PutObjectCommand,\n PutObjectCommandInput,\n S3Client\n} from '@aws-sdk/client-s3';\nimport {\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport { Readable } from 'stream';\n\n/**\n * Options for configuring the S3ObjectStore.\n *\n * @example\n * const options: S3ObjectStoreOptions = {\n * bucket: 'my-bucket',\n * clientConfig: { region: 'us-west-2' }\n * };\n */\ninterface S3ObjectStoreOptions {\n /** The S3 bucket name. */\n bucket: string;\n /** Optional existing S3 client instance. */\n client?: S3Client;\n /** Optional configuration for creating a new S3 client. */\n clientConfig?: ConstructorParameters<typeof S3Client>[0];\n}\n\n/**\n * S3-backed implementation of the ObjectStore interface.\n * Provides methods for storing, retrieving, streaming, and deleting objects in S3.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n * const user = await store.readObject<{ name: string }>('user-1');\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n\n /**\n * Creates a new S3ObjectStore instance.\n * @param openTelemetryCollector - Collector for OpenTelemetry metrics.\n * @param options - S3 configuration options.\n * @param telemetryOptions - Telemetry configuration options.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n */\n constructor(\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: S3ObjectStoreOptions,\n private telemetryOptions: TelemetryOptions\n ) {\n this.s3 = options.client || new S3Client(options.clientConfig || {});\n this.bucket = options.bucket;\n this.initialized = false;\n }\n\n private async ensureBucketExists() {\n try {\n await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));\n } catch {\n await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket }));\n }\n\n this.initialized = true;\n }\n\n /**\n * Stores an object in the S3 bucket.\n * @template T - The type of the object being stored.\n * @param object - The object to store. Must include a `key` property.\n *\n * @example\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n */\n async putObject<T>(object: T & { key: string }): Promise<void> {\n if (!this.initialized) {\n await this.ensureBucketExists();\n }\n\n const { key, ...rest } = object;\n const params: PutObjectCommandInput = {\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(rest),\n ContentType: 'application/json'\n };\n await this.s3.send(new PutObjectCommand(params));\n }\n\n /**\n * Stores multiple objects in the S3 bucket.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to store. Each must include a `key` property.\n *\n * @example\n * await store.putBatchObjects([\n * { key: 'user-1', name: 'Alice' },\n * { key: 'user-2', name: 'Bob' }\n * ]);\n */\n async putBatchObjects<T>(objects: (T & { key: string })[]): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj)));\n }\n\n /**\n * Streams an object upload to the S3 bucket.\n * For compatibility; uses putObject internally.\n * @template T - The type of the object being stored.\n * @param object - The object to stream-upload. Must include a `key` property.\n */\n async streamUploadObject<T>(object: T & { key: string }): Promise<void> {\n await this.putObject(object);\n }\n\n /**\n * Streams multiple object uploads to the S3 bucket.\n * For compatibility; uses putBatchObjects internally.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to stream-upload. Each must include a `key` property.\n */\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[]\n ): Promise<void> {\n await this.putBatchObjects(objects);\n }\n\n /**\n * Deletes an object from the S3 bucket.\n * @param objectKey - The key of the object to delete.\n *\n * @example\n * await store.deleteObject('user-1');\n */\n async deleteObject(objectKey: string): Promise<void> {\n await this.s3.send(\n new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n }\n\n /**\n * Deletes multiple objects from the S3 bucket.\n * @param objectKeys - The keys of the objects to delete.\n *\n * @example\n * await store.deleteBatchObjects(['user-1', 'user-2']);\n */\n async deleteBatchObjects(objectKeys: string[]): Promise<void> {\n const params: DeleteObjectsCommandInput = {\n Bucket: this.bucket,\n Delete: {\n Objects: objectKeys.map((Key) => ({ Key }))\n }\n };\n await this.s3.send(new DeleteObjectsCommand(params));\n }\n\n /**\n * Reads an object from the S3 bucket.\n * @template T - The expected type of the object.\n * @param objectKey - The key of the object to read.\n * @returns The parsed object.\n *\n * @example\n * const user = await store.readObject<{ name: string }>('user-1');\n */\n async readObject<T>(objectKey: string): Promise<T> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n\n if (!resp.Body) {\n throw new Error('S3 did not return a body');\n }\n\n return JSON.parse(await resp.Body.transformToString()) as T;\n }\n\n /**\n * Reads multiple objects from the S3 bucket.\n * @template T - The expected type of the objects.\n * @param objectKeys - The keys of the objects to read.\n * @returns An array of parsed objects.\n *\n * @example\n * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);\n */\n async readBatchObjects<T>(objectKeys: string[]): Promise<T[]> {\n return Promise.all(objectKeys.map((key) => this.readObject<T>(key)));\n }\n\n /**\n * Streams an object download from the S3 bucket.\n * @param objectKey - The key of the object to download.\n * @returns A readable stream of the object's contents.\n * @throws If the S3 response does not include a readable stream.\n *\n * @example\n * const stream = await store.streamDownloadObject('user-1');\n * stream.pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadObject(objectKey: string): Promise<Readable> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n const webStream = resp.Body?.transformToWebStream();\n if (!webStream) {\n throw new Error('S3 did not return a stream');\n }\n\n return Readable.fromWeb(\n webStream as Parameters<typeof Readable.fromWeb>[0]\n );\n }\n\n /**\n * Streams multiple object downloads from the S3 bucket.\n * @param objectKeys - The keys of the objects to download.\n * @returns An array of readable streams.\n *\n * @example\n * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);\n * streams[0].pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));\n }\n\n /**\n * Gets the underlying S3 client instance.\n * @returns The S3Client instance used by this store.\n *\n * @example\n * const s3Client = store.getClient();\n */\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import {\n CreateBucketCommand,\n DeleteObjectCommand,\n DeleteObjectsCommand,\n DeleteObjectsCommandInput,\n GetObjectCommand,\n HeadBucketCommand,\n PutObjectCommand,\n PutObjectCommandInput,\n S3Client\n} from '@aws-sdk/client-s3';\nimport {\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport {\n getCurrentTenantId,\n type FieldEncryptor\n} from '@forklaunch/core/persistence';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport { Readable } from 'stream';\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 S3 object store.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface S3EncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting object bodies. */\n encryptor: FieldEncryptor;\n /** Set to true to disable encryption. Defaults to false (encryption enabled). */\n disabled?: boolean;\n}\n\n/**\n * Options for configuring the S3ObjectStore.\n *\n * @example\n * const options: S3ObjectStoreOptions = {\n * bucket: 'my-bucket',\n * clientConfig: { region: 'us-west-2' }\n * };\n */\ninterface S3ObjectStoreOptions {\n /** The S3 bucket name. */\n bucket: string;\n /** Optional existing S3 client instance. */\n client?: S3Client;\n /** Optional configuration for creating a new S3 client. */\n clientConfig?: ConstructorParameters<typeof S3Client>[0];\n}\n\n/**\n * S3-backed implementation of the ObjectStore interface.\n * Provides methods for storing, retrieving, streaming, and deleting objects in S3.\n *\n * Encryption is enabled by default when an encryptor is provided. Object bodies\n * are encrypted before upload and decrypted after download using AES-256-GCM\n * with per-tenant key derivation.\n *\n * @example\n * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n * const user = await store.readObject<{ name: string }>('user-1');\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n private encryptor?: FieldEncryptor;\n private encryptionDisabled: boolean;\n\n /**\n * Creates a new S3ObjectStore instance.\n * @param openTelemetryCollector - Collector for OpenTelemetry metrics.\n * @param options - S3 configuration options.\n * @param telemetryOptions - Telemetry configuration options.\n * @param encryption - Encryption configuration (enabled by default when encryptor provided).\n *\n * @example\n * const store = new S3ObjectStore(\n * otelCollector,\n * { bucket: 'my-bucket' },\n * telemetryOptions,\n * { encryptor }\n * );\n */\n constructor(\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: S3ObjectStoreOptions,\n private telemetryOptions: TelemetryOptions,\n encryption: S3EncryptionOptions\n ) {\n this.s3 = options.client || new S3Client(options.clientConfig || {});\n this.bucket = options.bucket;\n this.initialized = false;\n this.encryptor = encryption.encryptor;\n this.encryptionDisabled = encryption.disabled ?? false;\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers\n // ---------------------------------------------------------------------------\n\n private encryptBody(body: string): string {\n if (!this.encryptor || this.encryptionDisabled) return body;\n return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;\n }\n\n private decryptBody(body: string): string {\n if (!this.encryptor || this.encryptionDisabled) return body;\n if (!isEncrypted(body)) return body;\n try {\n return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;\n } catch {\n return body;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------------------\n\n private async ensureBucketExists() {\n try {\n await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket }));\n } catch {\n await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket }));\n }\n\n this.initialized = true;\n }\n\n /**\n * Stores an object in the S3 bucket.\n * @template T - The type of the object being stored.\n * @param object - The object to store. Must include a `key` property.\n *\n * @example\n * await store.putObject({ key: 'user-1', name: 'Alice' });\n */\n async putObject<T>(object: T & { key: string }): Promise<void> {\n if (!this.initialized) {\n await this.ensureBucketExists();\n }\n\n const { key, ...rest } = object;\n const body = this.encryptBody(JSON.stringify(rest));\n const params: PutObjectCommandInput = {\n Bucket: this.bucket,\n Key: key,\n Body: body,\n ContentType: 'application/json'\n };\n await this.s3.send(new PutObjectCommand(params));\n }\n\n /**\n * Stores multiple objects in the S3 bucket.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to store. Each must include a `key` property.\n *\n * @example\n * await store.putBatchObjects([\n * { key: 'user-1', name: 'Alice' },\n * { key: 'user-2', name: 'Bob' }\n * ]);\n */\n async putBatchObjects<T>(objects: (T & { key: string })[]): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj)));\n }\n\n /**\n * Streams an object upload to the S3 bucket.\n * For compatibility; uses putObject internally.\n * @template T - The type of the object being stored.\n * @param object - The object to stream-upload. Must include a `key` property.\n */\n async streamUploadObject<T>(object: T & { key: string }): Promise<void> {\n await this.putObject(object);\n }\n\n /**\n * Streams multiple object uploads to the S3 bucket.\n * For compatibility; uses putBatchObjects internally.\n * @template T - The type of the objects being stored.\n * @param objects - The objects to stream-upload. Each must include a `key` property.\n */\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[]\n ): Promise<void> {\n await this.putBatchObjects(objects);\n }\n\n /**\n * Deletes an object from the S3 bucket.\n * @param objectKey - The key of the object to delete.\n *\n * @example\n * await store.deleteObject('user-1');\n */\n async deleteObject(objectKey: string): Promise<void> {\n await this.s3.send(\n new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n }\n\n /**\n * Deletes multiple objects from the S3 bucket.\n * @param objectKeys - The keys of the objects to delete.\n *\n * @example\n * await store.deleteBatchObjects(['user-1', 'user-2']);\n */\n async deleteBatchObjects(objectKeys: string[]): Promise<void> {\n const params: DeleteObjectsCommandInput = {\n Bucket: this.bucket,\n Delete: {\n Objects: objectKeys.map((Key) => ({ Key }))\n }\n };\n await this.s3.send(new DeleteObjectsCommand(params));\n }\n\n /**\n * Reads an object from the S3 bucket.\n * @template T - The expected type of the object.\n * @param objectKey - The key of the object to read.\n * @returns The parsed object.\n *\n * @example\n * const user = await store.readObject<{ name: string }>('user-1');\n */\n async readObject<T>(objectKey: string): Promise<T> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n\n if (!resp.Body) {\n throw new Error('S3 did not return a body');\n }\n\n const raw = await resp.Body.transformToString();\n return JSON.parse(this.decryptBody(raw)) as T;\n }\n\n /**\n * Reads multiple objects from the S3 bucket.\n * @template T - The expected type of the objects.\n * @param objectKeys - The keys of the objects to read.\n * @returns An array of parsed objects.\n *\n * @example\n * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);\n */\n async readBatchObjects<T>(objectKeys: string[]): Promise<T[]> {\n return Promise.all(objectKeys.map((key) => this.readObject<T>(key)));\n }\n\n /**\n * Streams an object download from the S3 bucket.\n * Note: Streaming bypasses application-level encryption/decryption.\n * Use readObject for encrypted objects.\n * @param objectKey - The key of the object to download.\n * @returns A readable stream of the object's contents.\n * @throws If the S3 response does not include a readable stream.\n *\n * @example\n * const stream = await store.streamDownloadObject('user-1');\n * stream.pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadObject(objectKey: string): Promise<Readable> {\n const resp = await this.s3.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })\n );\n const webStream = resp.Body?.transformToWebStream();\n if (!webStream) {\n throw new Error('S3 did not return a stream');\n }\n\n return Readable.fromWeb(\n webStream as Parameters<typeof Readable.fromWeb>[0]\n );\n }\n\n /**\n * Streams multiple object downloads from the S3 bucket.\n * Note: Streaming bypasses application-level encryption/decryption.\n * @param objectKeys - The keys of the objects to download.\n * @returns An array of readable streams.\n *\n * @example\n * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);\n * streams[0].pipe(fs.createWriteStream('user-1.json'));\n */\n async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(\n objectKeys.map((key) => this.streamDownloadObject(key))\n );\n }\n\n /**\n * Gets the underlying S3 client instance.\n * @returns The S3Client instance used by this store.\n *\n * @example\n * const s3Client = store.getClient();\n */\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forklaunch/infrastructure-s3",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "S3 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
|
"@aws-sdk/client-s3": "^3.1019.0",
|
|
32
|
-
"@forklaunch/
|
|
33
|
-
"@forklaunch/
|
|
32
|
+
"@forklaunch/common": "1.2.6",
|
|
33
|
+
"@forklaunch/core": "1.3.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^10.0.1",
|