@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.
@@ -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(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
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: JSON.stringify(rest),
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
- return JSON.parse(await resp.Body.transformToString()) as T;
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(objectKeys.map((key) => this.streamDownloadObject(key)));
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(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
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(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
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(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
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: JSON.stringify(rest),
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
- return JSON.parse(await resp.Body.transformToString());
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(objectKeys.map((key) => this.streamDownloadObject(key)));
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(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
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: JSON.stringify(rest),
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
- return JSON.parse(await resp.Body.transformToString());
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(objectKeys.map((key) => this.streamDownloadObject(key)));
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.2.9",
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/core": "1.2.9",
33
- "@forklaunch/common": "1.2.4"
32
+ "@forklaunch/common": "1.2.6",
33
+ "@forklaunch/core": "1.3.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/js": "^10.0.1",