@forklaunch/infrastructure-s3 1.3.14 → 1.4.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,11 +14,9 @@ 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
+ import { type FieldEncryptor } from '@forklaunch/core/persistence';
21
18
  import { ObjectStore } from '@forklaunch/core/objectstore';
19
+ import type { ComplianceContext } from '@forklaunch/core/cache';
22
20
  import { Readable } from 'stream';
23
21
 
24
22
  const ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;
@@ -34,18 +32,10 @@ function isEncrypted(value: string): boolean {
34
32
  export interface S3EncryptionOptions {
35
33
  /** The FieldEncryptor instance to use for encrypting object bodies. */
36
34
  encryptor: FieldEncryptor;
37
- /** Set to true to disable encryption. Defaults to false (encryption enabled). */
38
- disabled?: boolean;
39
35
  }
40
36
 
41
37
  /**
42
38
  * Options for configuring the S3ObjectStore.
43
- *
44
- * @example
45
- * const options: S3ObjectStoreOptions = {
46
- * bucket: 'my-bucket',
47
- * clientConfig: { region: 'us-west-2' }
48
- * };
49
39
  */
50
40
  interface S3ObjectStoreOptions {
51
41
  /** The S3 bucket name. */
@@ -60,37 +50,15 @@ interface S3ObjectStoreOptions {
60
50
  * S3-backed implementation of the ObjectStore interface.
61
51
  * Provides methods for storing, retrieving, streaming, and deleting objects in S3.
62
52
  *
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
- *
67
- * @example
68
- * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
69
- * await store.putObject({ key: 'user-1', name: 'Alice' });
70
- * const user = await store.readObject<{ name: string }>('user-1');
53
+ * Encryption is activated per-operation when a `compliance` context is provided.
54
+ * Without it, object bodies are stored and read as plaintext.
71
55
  */
72
56
  export class S3ObjectStore implements ObjectStore<S3Client> {
73
57
  private s3: S3Client;
74
58
  private bucket: string;
75
59
  private initialized: boolean;
76
60
  private encryptor?: FieldEncryptor;
77
- private encryptionDisabled: boolean;
78
61
 
79
- /**
80
- * Creates a new S3ObjectStore instance.
81
- * @param openTelemetryCollector - Collector for OpenTelemetry metrics.
82
- * @param options - S3 configuration options.
83
- * @param telemetryOptions - Telemetry configuration options.
84
- * @param encryption - Encryption configuration (enabled by default when encryptor provided).
85
- *
86
- * @example
87
- * const store = new S3ObjectStore(
88
- * otelCollector,
89
- * { bucket: 'my-bucket' },
90
- * telemetryOptions,
91
- * { encryptor }
92
- * );
93
- */
94
62
  constructor(
95
63
  private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,
96
64
  options: S3ObjectStoreOptions,
@@ -101,23 +69,22 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
101
69
  this.bucket = options.bucket;
102
70
  this.initialized = false;
103
71
  this.encryptor = encryption.encryptor;
104
- this.encryptionDisabled = encryption.disabled ?? false;
105
72
  }
106
73
 
107
74
  // ---------------------------------------------------------------------------
108
- // Encryption helpers
75
+ // Encryption helpers — only active when compliance context is provided
109
76
  // ---------------------------------------------------------------------------
110
77
 
111
- private encryptBody(body: string): string {
112
- if (!this.encryptor || this.encryptionDisabled) return body;
113
- return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;
78
+ private encryptBody(body: string, compliance?: ComplianceContext): string {
79
+ if (!compliance || !this.encryptor) return body;
80
+ return this.encryptor.encrypt(body, compliance.tenantId) ?? body;
114
81
  }
115
82
 
116
- private decryptBody(body: string): string {
117
- if (!this.encryptor || this.encryptionDisabled) return body;
83
+ private decryptBody(body: string, compliance?: ComplianceContext): string {
84
+ if (!compliance || !this.encryptor) return body;
118
85
  if (!isEncrypted(body)) return body;
119
86
  try {
120
- return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;
87
+ return this.encryptor.decrypt(body, compliance.tenantId) ?? body;
121
88
  } catch {
122
89
  return body;
123
90
  }
@@ -137,21 +104,16 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
137
104
  this.initialized = true;
138
105
  }
139
106
 
140
- /**
141
- * Stores an object in the S3 bucket.
142
- * @template T - The type of the object being stored.
143
- * @param object - The object to store. Must include a `key` property.
144
- *
145
- * @example
146
- * await store.putObject({ key: 'user-1', name: 'Alice' });
147
- */
148
- async putObject<T>(object: T & { key: string }): Promise<void> {
107
+ async putObject<T>(
108
+ object: T & { key: string },
109
+ compliance?: ComplianceContext
110
+ ): Promise<void> {
149
111
  if (!this.initialized) {
150
112
  await this.ensureBucketExists();
151
113
  }
152
114
 
153
115
  const { key, ...rest } = object;
154
- const body = this.encryptBody(JSON.stringify(rest));
116
+ const body = this.encryptBody(JSON.stringify(rest), compliance);
155
117
  const params: PutObjectCommandInput = {
156
118
  Bucket: this.bucket,
157
119
  Key: key,
@@ -161,63 +123,33 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
161
123
  await this.s3.send(new PutObjectCommand(params));
162
124
  }
163
125
 
164
- /**
165
- * Stores multiple objects in the S3 bucket.
166
- * @template T - The type of the objects being stored.
167
- * @param objects - The objects to store. Each must include a `key` property.
168
- *
169
- * @example
170
- * await store.putBatchObjects([
171
- * { key: 'user-1', name: 'Alice' },
172
- * { key: 'user-2', name: 'Bob' }
173
- * ]);
174
- */
175
- async putBatchObjects<T>(objects: (T & { key: string })[]): Promise<void> {
176
- await Promise.all(objects.map((obj) => this.putObject(obj)));
126
+ async putBatchObjects<T>(
127
+ objects: (T & { key: string })[],
128
+ compliance?: ComplianceContext
129
+ ): Promise<void> {
130
+ await Promise.all(objects.map((obj) => this.putObject(obj, compliance)));
177
131
  }
178
132
 
179
- /**
180
- * Streams an object upload to the S3 bucket.
181
- * For compatibility; uses putObject internally.
182
- * @template T - The type of the object being stored.
183
- * @param object - The object to stream-upload. Must include a `key` property.
184
- */
185
- async streamUploadObject<T>(object: T & { key: string }): Promise<void> {
186
- await this.putObject(object);
133
+ async streamUploadObject<T>(
134
+ object: T & { key: string },
135
+ compliance?: ComplianceContext
136
+ ): Promise<void> {
137
+ await this.putObject(object, compliance);
187
138
  }
188
139
 
189
- /**
190
- * Streams multiple object uploads to the S3 bucket.
191
- * For compatibility; uses putBatchObjects internally.
192
- * @template T - The type of the objects being stored.
193
- * @param objects - The objects to stream-upload. Each must include a `key` property.
194
- */
195
140
  async streamUploadBatchObjects<T>(
196
- objects: (T & { key: string })[]
141
+ objects: (T & { key: string })[],
142
+ compliance?: ComplianceContext
197
143
  ): Promise<void> {
198
- await this.putBatchObjects(objects);
144
+ await this.putBatchObjects(objects, compliance);
199
145
  }
200
146
 
201
- /**
202
- * Deletes an object from the S3 bucket.
203
- * @param objectKey - The key of the object to delete.
204
- *
205
- * @example
206
- * await store.deleteObject('user-1');
207
- */
208
147
  async deleteObject(objectKey: string): Promise<void> {
209
148
  await this.s3.send(
210
149
  new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })
211
150
  );
212
151
  }
213
152
 
214
- /**
215
- * Deletes multiple objects from the S3 bucket.
216
- * @param objectKeys - The keys of the objects to delete.
217
- *
218
- * @example
219
- * await store.deleteBatchObjects(['user-1', 'user-2']);
220
- */
221
153
  async deleteBatchObjects(objectKeys: string[]): Promise<void> {
222
154
  const params: DeleteObjectsCommandInput = {
223
155
  Bucket: this.bucket,
@@ -228,16 +160,10 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
228
160
  await this.s3.send(new DeleteObjectsCommand(params));
229
161
  }
230
162
 
231
- /**
232
- * Reads an object from the S3 bucket.
233
- * @template T - The expected type of the object.
234
- * @param objectKey - The key of the object to read.
235
- * @returns The parsed object.
236
- *
237
- * @example
238
- * const user = await store.readObject<{ name: string }>('user-1');
239
- */
240
- async readObject<T>(objectKey: string): Promise<T> {
163
+ async readObject<T>(
164
+ objectKey: string,
165
+ compliance?: ComplianceContext
166
+ ): Promise<T> {
241
167
  const resp = await this.s3.send(
242
168
  new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
243
169
  );
@@ -247,34 +173,18 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
247
173
  }
248
174
 
249
175
  const raw = await resp.Body.transformToString();
250
- return JSON.parse(this.decryptBody(raw)) as T;
176
+ return JSON.parse(this.decryptBody(raw, compliance)) as T;
251
177
  }
252
178
 
253
- /**
254
- * Reads multiple objects from the S3 bucket.
255
- * @template T - The expected type of the objects.
256
- * @param objectKeys - The keys of the objects to read.
257
- * @returns An array of parsed objects.
258
- *
259
- * @example
260
- * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);
261
- */
262
- async readBatchObjects<T>(objectKeys: string[]): Promise<T[]> {
263
- return Promise.all(objectKeys.map((key) => this.readObject<T>(key)));
179
+ async readBatchObjects<T>(
180
+ objectKeys: string[],
181
+ compliance?: ComplianceContext
182
+ ): Promise<T[]> {
183
+ return Promise.all(
184
+ objectKeys.map((key) => this.readObject<T>(key, compliance))
185
+ );
264
186
  }
265
187
 
266
- /**
267
- * Streams an object download from the S3 bucket.
268
- * Note: Streaming bypasses application-level encryption/decryption.
269
- * Use readObject for encrypted objects.
270
- * @param objectKey - The key of the object to download.
271
- * @returns A readable stream of the object's contents.
272
- * @throws If the S3 response does not include a readable stream.
273
- *
274
- * @example
275
- * const stream = await store.streamDownloadObject('user-1');
276
- * stream.pipe(fs.createWriteStream('user-1.json'));
277
- */
278
188
  async streamDownloadObject(objectKey: string): Promise<Readable> {
279
189
  const resp = await this.s3.send(
280
190
  new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
@@ -289,27 +199,10 @@ export class S3ObjectStore implements ObjectStore<S3Client> {
289
199
  );
290
200
  }
291
201
 
292
- /**
293
- * Streams multiple object downloads from the S3 bucket.
294
- * Note: Streaming bypasses application-level encryption/decryption.
295
- * @param objectKeys - The keys of the objects to download.
296
- * @returns An array of readable streams.
297
- *
298
- * @example
299
- * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);
300
- * streams[0].pipe(fs.createWriteStream('user-1.json'));
301
- */
302
202
  async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {
303
203
  return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));
304
204
  }
305
205
 
306
- /**
307
- * Gets the underlying S3 client instance.
308
- * @returns The S3Client instance used by this store.
309
- *
310
- * @example
311
- * const s3Client = store.getClient();
312
- */
313
206
  getClient(): S3Client {
314
207
  return this.s3;
315
208
  }
package/lib/index.d.mts CHANGED
@@ -2,6 +2,7 @@ import { S3Client } from '@aws-sdk/client-s3';
2
2
  import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
3
3
  import { FieldEncryptor } from '@forklaunch/core/persistence';
4
4
  import { ObjectStore } from '@forklaunch/core/objectstore';
5
+ import { ComplianceContext } from '@forklaunch/core/cache';
5
6
  import { Readable } from 'stream';
6
7
 
7
8
  /**
@@ -11,17 +12,9 @@ import { Readable } from 'stream';
11
12
  interface S3EncryptionOptions {
12
13
  /** The FieldEncryptor instance to use for encrypting object bodies. */
13
14
  encryptor: FieldEncryptor;
14
- /** Set to true to disable encryption. Defaults to false (encryption enabled). */
15
- disabled?: boolean;
16
15
  }
17
16
  /**
18
17
  * Options for configuring the S3ObjectStore.
19
- *
20
- * @example
21
- * const options: S3ObjectStoreOptions = {
22
- * bucket: 'my-bucket',
23
- * clientConfig: { region: 'us-west-2' }
24
- * };
25
18
  */
26
19
  interface S3ObjectStoreOptions {
27
20
  /** The S3 bucket name. */
@@ -35,14 +28,8 @@ interface S3ObjectStoreOptions {
35
28
  * S3-backed implementation of the ObjectStore interface.
36
29
  * Provides methods for storing, retrieving, streaming, and deleting objects in S3.
37
30
  *
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
- *
42
- * @example
43
- * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
44
- * await store.putObject({ key: 'user-1', name: 'Alice' });
45
- * const user = await store.readObject<{ name: string }>('user-1');
31
+ * Encryption is activated per-operation when a `compliance` context is provided.
32
+ * Without it, object bodies are stored and read as plaintext.
46
33
  */
47
34
  declare class S3ObjectStore implements ObjectStore<S3Client> {
48
35
  private openTelemetryCollector;
@@ -51,136 +38,28 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
51
38
  private bucket;
52
39
  private initialized;
53
40
  private encryptor?;
54
- private encryptionDisabled;
55
- /**
56
- * Creates a new S3ObjectStore instance.
57
- * @param openTelemetryCollector - Collector for OpenTelemetry metrics.
58
- * @param options - S3 configuration options.
59
- * @param telemetryOptions - Telemetry configuration options.
60
- * @param encryption - Encryption configuration (enabled by default when encryptor provided).
61
- *
62
- * @example
63
- * const store = new S3ObjectStore(
64
- * otelCollector,
65
- * { bucket: 'my-bucket' },
66
- * telemetryOptions,
67
- * { encryptor }
68
- * );
69
- */
70
41
  constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions, encryption: S3EncryptionOptions);
71
42
  private encryptBody;
72
43
  private decryptBody;
73
44
  private ensureBucketExists;
74
- /**
75
- * Stores an object in the S3 bucket.
76
- * @template T - The type of the object being stored.
77
- * @param object - The object to store. Must include a `key` property.
78
- *
79
- * @example
80
- * await store.putObject({ key: 'user-1', name: 'Alice' });
81
- */
82
45
  putObject<T>(object: T & {
83
46
  key: string;
84
- }): Promise<void>;
85
- /**
86
- * Stores multiple objects in the S3 bucket.
87
- * @template T - The type of the objects being stored.
88
- * @param objects - The objects to store. Each must include a `key` property.
89
- *
90
- * @example
91
- * await store.putBatchObjects([
92
- * { key: 'user-1', name: 'Alice' },
93
- * { key: 'user-2', name: 'Bob' }
94
- * ]);
95
- */
47
+ }, compliance?: ComplianceContext): Promise<void>;
96
48
  putBatchObjects<T>(objects: (T & {
97
49
  key: string;
98
- })[]): Promise<void>;
99
- /**
100
- * Streams an object upload to the S3 bucket.
101
- * For compatibility; uses putObject internally.
102
- * @template T - The type of the object being stored.
103
- * @param object - The object to stream-upload. Must include a `key` property.
104
- */
50
+ })[], compliance?: ComplianceContext): Promise<void>;
105
51
  streamUploadObject<T>(object: T & {
106
52
  key: string;
107
- }): Promise<void>;
108
- /**
109
- * Streams multiple object uploads to the S3 bucket.
110
- * For compatibility; uses putBatchObjects internally.
111
- * @template T - The type of the objects being stored.
112
- * @param objects - The objects to stream-upload. Each must include a `key` property.
113
- */
53
+ }, compliance?: ComplianceContext): Promise<void>;
114
54
  streamUploadBatchObjects<T>(objects: (T & {
115
55
  key: string;
116
- })[]): Promise<void>;
117
- /**
118
- * Deletes an object from the S3 bucket.
119
- * @param objectKey - The key of the object to delete.
120
- *
121
- * @example
122
- * await store.deleteObject('user-1');
123
- */
56
+ })[], compliance?: ComplianceContext): Promise<void>;
124
57
  deleteObject(objectKey: string): Promise<void>;
125
- /**
126
- * Deletes multiple objects from the S3 bucket.
127
- * @param objectKeys - The keys of the objects to delete.
128
- *
129
- * @example
130
- * await store.deleteBatchObjects(['user-1', 'user-2']);
131
- */
132
58
  deleteBatchObjects(objectKeys: string[]): Promise<void>;
133
- /**
134
- * Reads an object from the S3 bucket.
135
- * @template T - The expected type of the object.
136
- * @param objectKey - The key of the object to read.
137
- * @returns The parsed object.
138
- *
139
- * @example
140
- * const user = await store.readObject<{ name: string }>('user-1');
141
- */
142
- readObject<T>(objectKey: string): Promise<T>;
143
- /**
144
- * Reads multiple objects from the S3 bucket.
145
- * @template T - The expected type of the objects.
146
- * @param objectKeys - The keys of the objects to read.
147
- * @returns An array of parsed objects.
148
- *
149
- * @example
150
- * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);
151
- */
152
- readBatchObjects<T>(objectKeys: string[]): Promise<T[]>;
153
- /**
154
- * Streams an object download from the S3 bucket.
155
- * Note: Streaming bypasses application-level encryption/decryption.
156
- * Use readObject for encrypted objects.
157
- * @param objectKey - The key of the object to download.
158
- * @returns A readable stream of the object's contents.
159
- * @throws If the S3 response does not include a readable stream.
160
- *
161
- * @example
162
- * const stream = await store.streamDownloadObject('user-1');
163
- * stream.pipe(fs.createWriteStream('user-1.json'));
164
- */
59
+ readObject<T>(objectKey: string, compliance?: ComplianceContext): Promise<T>;
60
+ readBatchObjects<T>(objectKeys: string[], compliance?: ComplianceContext): Promise<T[]>;
165
61
  streamDownloadObject(objectKey: string): Promise<Readable>;
166
- /**
167
- * Streams multiple object downloads from the S3 bucket.
168
- * Note: Streaming bypasses application-level encryption/decryption.
169
- * @param objectKeys - The keys of the objects to download.
170
- * @returns An array of readable streams.
171
- *
172
- * @example
173
- * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);
174
- * streams[0].pipe(fs.createWriteStream('user-1.json'));
175
- */
176
62
  streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]>;
177
- /**
178
- * Gets the underlying S3 client instance.
179
- * @returns The S3Client instance used by this store.
180
- *
181
- * @example
182
- * const s3Client = store.getClient();
183
- */
184
63
  getClient(): S3Client;
185
64
  }
186
65
 
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { S3Client } from '@aws-sdk/client-s3';
2
2
  import { OpenTelemetryCollector, MetricsDefinition, TelemetryOptions } from '@forklaunch/core/http';
3
3
  import { FieldEncryptor } from '@forklaunch/core/persistence';
4
4
  import { ObjectStore } from '@forklaunch/core/objectstore';
5
+ import { ComplianceContext } from '@forklaunch/core/cache';
5
6
  import { Readable } from 'stream';
6
7
 
7
8
  /**
@@ -11,17 +12,9 @@ import { Readable } from 'stream';
11
12
  interface S3EncryptionOptions {
12
13
  /** The FieldEncryptor instance to use for encrypting object bodies. */
13
14
  encryptor: FieldEncryptor;
14
- /** Set to true to disable encryption. Defaults to false (encryption enabled). */
15
- disabled?: boolean;
16
15
  }
17
16
  /**
18
17
  * Options for configuring the S3ObjectStore.
19
- *
20
- * @example
21
- * const options: S3ObjectStoreOptions = {
22
- * bucket: 'my-bucket',
23
- * clientConfig: { region: 'us-west-2' }
24
- * };
25
18
  */
26
19
  interface S3ObjectStoreOptions {
27
20
  /** The S3 bucket name. */
@@ -35,14 +28,8 @@ interface S3ObjectStoreOptions {
35
28
  * S3-backed implementation of the ObjectStore interface.
36
29
  * Provides methods for storing, retrieving, streaming, and deleting objects in S3.
37
30
  *
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
- *
42
- * @example
43
- * const store = new S3ObjectStore(otelCollector, { bucket: 'my-bucket' }, telemetryOptions);
44
- * await store.putObject({ key: 'user-1', name: 'Alice' });
45
- * const user = await store.readObject<{ name: string }>('user-1');
31
+ * Encryption is activated per-operation when a `compliance` context is provided.
32
+ * Without it, object bodies are stored and read as plaintext.
46
33
  */
47
34
  declare class S3ObjectStore implements ObjectStore<S3Client> {
48
35
  private openTelemetryCollector;
@@ -51,136 +38,28 @@ declare class S3ObjectStore implements ObjectStore<S3Client> {
51
38
  private bucket;
52
39
  private initialized;
53
40
  private encryptor?;
54
- private encryptionDisabled;
55
- /**
56
- * Creates a new S3ObjectStore instance.
57
- * @param openTelemetryCollector - Collector for OpenTelemetry metrics.
58
- * @param options - S3 configuration options.
59
- * @param telemetryOptions - Telemetry configuration options.
60
- * @param encryption - Encryption configuration (enabled by default when encryptor provided).
61
- *
62
- * @example
63
- * const store = new S3ObjectStore(
64
- * otelCollector,
65
- * { bucket: 'my-bucket' },
66
- * telemetryOptions,
67
- * { encryptor }
68
- * );
69
- */
70
41
  constructor(openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>, options: S3ObjectStoreOptions, telemetryOptions: TelemetryOptions, encryption: S3EncryptionOptions);
71
42
  private encryptBody;
72
43
  private decryptBody;
73
44
  private ensureBucketExists;
74
- /**
75
- * Stores an object in the S3 bucket.
76
- * @template T - The type of the object being stored.
77
- * @param object - The object to store. Must include a `key` property.
78
- *
79
- * @example
80
- * await store.putObject({ key: 'user-1', name: 'Alice' });
81
- */
82
45
  putObject<T>(object: T & {
83
46
  key: string;
84
- }): Promise<void>;
85
- /**
86
- * Stores multiple objects in the S3 bucket.
87
- * @template T - The type of the objects being stored.
88
- * @param objects - The objects to store. Each must include a `key` property.
89
- *
90
- * @example
91
- * await store.putBatchObjects([
92
- * { key: 'user-1', name: 'Alice' },
93
- * { key: 'user-2', name: 'Bob' }
94
- * ]);
95
- */
47
+ }, compliance?: ComplianceContext): Promise<void>;
96
48
  putBatchObjects<T>(objects: (T & {
97
49
  key: string;
98
- })[]): Promise<void>;
99
- /**
100
- * Streams an object upload to the S3 bucket.
101
- * For compatibility; uses putObject internally.
102
- * @template T - The type of the object being stored.
103
- * @param object - The object to stream-upload. Must include a `key` property.
104
- */
50
+ })[], compliance?: ComplianceContext): Promise<void>;
105
51
  streamUploadObject<T>(object: T & {
106
52
  key: string;
107
- }): Promise<void>;
108
- /**
109
- * Streams multiple object uploads to the S3 bucket.
110
- * For compatibility; uses putBatchObjects internally.
111
- * @template T - The type of the objects being stored.
112
- * @param objects - The objects to stream-upload. Each must include a `key` property.
113
- */
53
+ }, compliance?: ComplianceContext): Promise<void>;
114
54
  streamUploadBatchObjects<T>(objects: (T & {
115
55
  key: string;
116
- })[]): Promise<void>;
117
- /**
118
- * Deletes an object from the S3 bucket.
119
- * @param objectKey - The key of the object to delete.
120
- *
121
- * @example
122
- * await store.deleteObject('user-1');
123
- */
56
+ })[], compliance?: ComplianceContext): Promise<void>;
124
57
  deleteObject(objectKey: string): Promise<void>;
125
- /**
126
- * Deletes multiple objects from the S3 bucket.
127
- * @param objectKeys - The keys of the objects to delete.
128
- *
129
- * @example
130
- * await store.deleteBatchObjects(['user-1', 'user-2']);
131
- */
132
58
  deleteBatchObjects(objectKeys: string[]): Promise<void>;
133
- /**
134
- * Reads an object from the S3 bucket.
135
- * @template T - The expected type of the object.
136
- * @param objectKey - The key of the object to read.
137
- * @returns The parsed object.
138
- *
139
- * @example
140
- * const user = await store.readObject<{ name: string }>('user-1');
141
- */
142
- readObject<T>(objectKey: string): Promise<T>;
143
- /**
144
- * Reads multiple objects from the S3 bucket.
145
- * @template T - The expected type of the objects.
146
- * @param objectKeys - The keys of the objects to read.
147
- * @returns An array of parsed objects.
148
- *
149
- * @example
150
- * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);
151
- */
152
- readBatchObjects<T>(objectKeys: string[]): Promise<T[]>;
153
- /**
154
- * Streams an object download from the S3 bucket.
155
- * Note: Streaming bypasses application-level encryption/decryption.
156
- * Use readObject for encrypted objects.
157
- * @param objectKey - The key of the object to download.
158
- * @returns A readable stream of the object's contents.
159
- * @throws If the S3 response does not include a readable stream.
160
- *
161
- * @example
162
- * const stream = await store.streamDownloadObject('user-1');
163
- * stream.pipe(fs.createWriteStream('user-1.json'));
164
- */
59
+ readObject<T>(objectKey: string, compliance?: ComplianceContext): Promise<T>;
60
+ readBatchObjects<T>(objectKeys: string[], compliance?: ComplianceContext): Promise<T[]>;
165
61
  streamDownloadObject(objectKey: string): Promise<Readable>;
166
- /**
167
- * Streams multiple object downloads from the S3 bucket.
168
- * Note: Streaming bypasses application-level encryption/decryption.
169
- * @param objectKeys - The keys of the objects to download.
170
- * @returns An array of readable streams.
171
- *
172
- * @example
173
- * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);
174
- * streams[0].pipe(fs.createWriteStream('user-1.json'));
175
- */
176
62
  streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]>;
177
- /**
178
- * Gets the underlying S3 client instance.
179
- * @returns The S3Client instance used by this store.
180
- *
181
- * @example
182
- * const s3Client = store.getClient();
183
- */
184
63
  getClient(): S3Client;
185
64
  }
186
65
 
package/lib/index.js CHANGED
@@ -24,28 +24,12 @@ __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");
28
27
  var import_stream = require("stream");
29
28
  var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
30
29
  function isEncrypted(value) {
31
30
  return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
32
31
  }
33
32
  var S3ObjectStore = class {
34
- /**
35
- * Creates a new S3ObjectStore instance.
36
- * @param openTelemetryCollector - Collector for OpenTelemetry metrics.
37
- * @param options - S3 configuration options.
38
- * @param telemetryOptions - Telemetry configuration options.
39
- * @param encryption - Encryption configuration (enabled by default when encryptor provided).
40
- *
41
- * @example
42
- * const store = new S3ObjectStore(
43
- * otelCollector,
44
- * { bucket: 'my-bucket' },
45
- * telemetryOptions,
46
- * { encryptor }
47
- * );
48
- */
49
33
  constructor(openTelemetryCollector, options, telemetryOptions, encryption) {
50
34
  this.openTelemetryCollector = openTelemetryCollector;
51
35
  this.telemetryOptions = telemetryOptions;
@@ -53,25 +37,25 @@ var S3ObjectStore = class {
53
37
  this.bucket = options.bucket;
54
38
  this.initialized = false;
55
39
  this.encryptor = encryption.encryptor;
56
- this.encryptionDisabled = encryption.disabled ?? false;
57
40
  }
41
+ openTelemetryCollector;
42
+ telemetryOptions;
58
43
  s3;
59
44
  bucket;
60
45
  initialized;
61
46
  encryptor;
62
- encryptionDisabled;
63
47
  // ---------------------------------------------------------------------------
64
- // Encryption helpers
48
+ // Encryption helpers — only active when compliance context is provided
65
49
  // ---------------------------------------------------------------------------
66
- encryptBody(body) {
67
- if (!this.encryptor || this.encryptionDisabled) return body;
68
- return this.encryptor.encrypt(body, (0, import_persistence.getCurrentTenantId)()) ?? body;
50
+ encryptBody(body, compliance) {
51
+ if (!compliance || !this.encryptor) return body;
52
+ return this.encryptor.encrypt(body, compliance.tenantId) ?? body;
69
53
  }
70
- decryptBody(body) {
71
- if (!this.encryptor || this.encryptionDisabled) return body;
54
+ decryptBody(body, compliance) {
55
+ if (!compliance || !this.encryptor) return body;
72
56
  if (!isEncrypted(body)) return body;
73
57
  try {
74
- return this.encryptor.decrypt(body, (0, import_persistence.getCurrentTenantId)()) ?? body;
58
+ return this.encryptor.decrypt(body, compliance.tenantId) ?? body;
75
59
  } catch {
76
60
  return body;
77
61
  }
@@ -87,20 +71,12 @@ var S3ObjectStore = class {
87
71
  }
88
72
  this.initialized = true;
89
73
  }
90
- /**
91
- * Stores an object in the S3 bucket.
92
- * @template T - The type of the object being stored.
93
- * @param object - The object to store. Must include a `key` property.
94
- *
95
- * @example
96
- * await store.putObject({ key: 'user-1', name: 'Alice' });
97
- */
98
- async putObject(object) {
74
+ async putObject(object, compliance) {
99
75
  if (!this.initialized) {
100
76
  await this.ensureBucketExists();
101
77
  }
102
78
  const { key, ...rest } = object;
103
- const body = this.encryptBody(JSON.stringify(rest));
79
+ const body = this.encryptBody(JSON.stringify(rest), compliance);
104
80
  const params = {
105
81
  Bucket: this.bucket,
106
82
  Key: key,
@@ -109,57 +85,20 @@ var S3ObjectStore = class {
109
85
  };
110
86
  await this.s3.send(new import_client_s3.PutObjectCommand(params));
111
87
  }
112
- /**
113
- * Stores multiple objects in the S3 bucket.
114
- * @template T - The type of the objects being stored.
115
- * @param objects - The objects to store. Each must include a `key` property.
116
- *
117
- * @example
118
- * await store.putBatchObjects([
119
- * { key: 'user-1', name: 'Alice' },
120
- * { key: 'user-2', name: 'Bob' }
121
- * ]);
122
- */
123
- async putBatchObjects(objects) {
124
- await Promise.all(objects.map((obj) => this.putObject(obj)));
125
- }
126
- /**
127
- * Streams an object upload to the S3 bucket.
128
- * For compatibility; uses putObject internally.
129
- * @template T - The type of the object being stored.
130
- * @param object - The object to stream-upload. Must include a `key` property.
131
- */
132
- async streamUploadObject(object) {
133
- await this.putObject(object);
134
- }
135
- /**
136
- * Streams multiple object uploads to the S3 bucket.
137
- * For compatibility; uses putBatchObjects internally.
138
- * @template T - The type of the objects being stored.
139
- * @param objects - The objects to stream-upload. Each must include a `key` property.
140
- */
141
- async streamUploadBatchObjects(objects) {
142
- await this.putBatchObjects(objects);
143
- }
144
- /**
145
- * Deletes an object from the S3 bucket.
146
- * @param objectKey - The key of the object to delete.
147
- *
148
- * @example
149
- * await store.deleteObject('user-1');
150
- */
88
+ async putBatchObjects(objects, compliance) {
89
+ await Promise.all(objects.map((obj) => this.putObject(obj, compliance)));
90
+ }
91
+ async streamUploadObject(object, compliance) {
92
+ await this.putObject(object, compliance);
93
+ }
94
+ async streamUploadBatchObjects(objects, compliance) {
95
+ await this.putBatchObjects(objects, compliance);
96
+ }
151
97
  async deleteObject(objectKey) {
152
98
  await this.s3.send(
153
99
  new import_client_s3.DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })
154
100
  );
155
101
  }
156
- /**
157
- * Deletes multiple objects from the S3 bucket.
158
- * @param objectKeys - The keys of the objects to delete.
159
- *
160
- * @example
161
- * await store.deleteBatchObjects(['user-1', 'user-2']);
162
- */
163
102
  async deleteBatchObjects(objectKeys) {
164
103
  const params = {
165
104
  Bucket: this.bucket,
@@ -169,16 +108,7 @@ var S3ObjectStore = class {
169
108
  };
170
109
  await this.s3.send(new import_client_s3.DeleteObjectsCommand(params));
171
110
  }
172
- /**
173
- * Reads an object from the S3 bucket.
174
- * @template T - The expected type of the object.
175
- * @param objectKey - The key of the object to read.
176
- * @returns The parsed object.
177
- *
178
- * @example
179
- * const user = await store.readObject<{ name: string }>('user-1');
180
- */
181
- async readObject(objectKey) {
111
+ async readObject(objectKey, compliance) {
182
112
  const resp = await this.s3.send(
183
113
  new import_client_s3.GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
184
114
  );
@@ -186,32 +116,13 @@ var S3ObjectStore = class {
186
116
  throw new Error("S3 did not return a body");
187
117
  }
188
118
  const raw = await resp.Body.transformToString();
189
- return JSON.parse(this.decryptBody(raw));
190
- }
191
- /**
192
- * Reads multiple objects from the S3 bucket.
193
- * @template T - The expected type of the objects.
194
- * @param objectKeys - The keys of the objects to read.
195
- * @returns An array of parsed objects.
196
- *
197
- * @example
198
- * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);
199
- */
200
- async readBatchObjects(objectKeys) {
201
- return Promise.all(objectKeys.map((key) => this.readObject(key)));
202
- }
203
- /**
204
- * Streams an object download from the S3 bucket.
205
- * Note: Streaming bypasses application-level encryption/decryption.
206
- * Use readObject for encrypted objects.
207
- * @param objectKey - The key of the object to download.
208
- * @returns A readable stream of the object's contents.
209
- * @throws If the S3 response does not include a readable stream.
210
- *
211
- * @example
212
- * const stream = await store.streamDownloadObject('user-1');
213
- * stream.pipe(fs.createWriteStream('user-1.json'));
214
- */
119
+ return JSON.parse(this.decryptBody(raw, compliance));
120
+ }
121
+ async readBatchObjects(objectKeys, compliance) {
122
+ return Promise.all(
123
+ objectKeys.map((key) => this.readObject(key, compliance))
124
+ );
125
+ }
215
126
  async streamDownloadObject(objectKey) {
216
127
  const resp = await this.s3.send(
217
128
  new import_client_s3.GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
@@ -224,26 +135,9 @@ var S3ObjectStore = class {
224
135
  webStream
225
136
  );
226
137
  }
227
- /**
228
- * Streams multiple object downloads from the S3 bucket.
229
- * Note: Streaming bypasses application-level encryption/decryption.
230
- * @param objectKeys - The keys of the objects to download.
231
- * @returns An array of readable streams.
232
- *
233
- * @example
234
- * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);
235
- * streams[0].pipe(fs.createWriteStream('user-1.json'));
236
- */
237
138
  async streamDownloadBatchObjects(objectKeys) {
238
139
  return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));
239
140
  }
240
- /**
241
- * Gets the underlying S3 client instance.
242
- * @returns The S3Client instance used by this store.
243
- *
244
- * @example
245
- * const s3Client = store.getClient();
246
- */
247
141
  getClient() {
248
142
  return this.s3;
249
143
  }
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 {\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(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 { type FieldEncryptor } from '@forklaunch/core/persistence';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport type { ComplianceContext } from '@forklaunch/core/cache';\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}\n\n/**\n * Options for configuring the S3ObjectStore.\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 activated per-operation when a `compliance` context is provided.\n * Without it, object bodies are stored and read as plaintext.\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n private encryptor?: FieldEncryptor;\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 }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers — only active when compliance context is provided\n // ---------------------------------------------------------------------------\n\n private encryptBody(body: string, compliance?: ComplianceContext): string {\n if (!compliance || !this.encryptor) return body;\n return this.encryptor.encrypt(body, compliance.tenantId) ?? body;\n }\n\n private decryptBody(body: string, compliance?: ComplianceContext): string {\n if (!compliance || !this.encryptor) return body;\n if (!isEncrypted(body)) return body;\n try {\n return this.encryptor.decrypt(body, compliance.tenantId) ?? 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 async putObject<T>(\n object: T & { key: string },\n compliance?: ComplianceContext\n ): 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), compliance);\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 async putBatchObjects<T>(\n objects: (T & { key: string })[],\n compliance?: ComplianceContext\n ): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj, compliance)));\n }\n\n async streamUploadObject<T>(\n object: T & { key: string },\n compliance?: ComplianceContext\n ): Promise<void> {\n await this.putObject(object, compliance);\n }\n\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[],\n compliance?: ComplianceContext\n ): Promise<void> {\n await this.putBatchObjects(objects, compliance);\n }\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 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 async readObject<T>(\n objectKey: string,\n compliance?: ComplianceContext\n ): 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, compliance)) as T;\n }\n\n async readBatchObjects<T>(\n objectKeys: string[],\n compliance?: ComplianceContext\n ): Promise<T[]> {\n return Promise.all(\n objectKeys.map((key) => this.readObject<T>(key, compliance))\n );\n }\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 async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));\n }\n\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":"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","names":[]}
package/lib/index.mjs CHANGED
@@ -8,30 +8,12 @@ import {
8
8
  PutObjectCommand,
9
9
  S3Client
10
10
  } from "@aws-sdk/client-s3";
11
- import {
12
- getCurrentTenantId
13
- } from "@forklaunch/core/persistence";
14
11
  import { Readable } from "stream";
15
12
  var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
16
13
  function isEncrypted(value) {
17
14
  return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));
18
15
  }
19
16
  var S3ObjectStore = class {
20
- /**
21
- * Creates a new S3ObjectStore instance.
22
- * @param openTelemetryCollector - Collector for OpenTelemetry metrics.
23
- * @param options - S3 configuration options.
24
- * @param telemetryOptions - Telemetry configuration options.
25
- * @param encryption - Encryption configuration (enabled by default when encryptor provided).
26
- *
27
- * @example
28
- * const store = new S3ObjectStore(
29
- * otelCollector,
30
- * { bucket: 'my-bucket' },
31
- * telemetryOptions,
32
- * { encryptor }
33
- * );
34
- */
35
17
  constructor(openTelemetryCollector, options, telemetryOptions, encryption) {
36
18
  this.openTelemetryCollector = openTelemetryCollector;
37
19
  this.telemetryOptions = telemetryOptions;
@@ -39,25 +21,25 @@ var S3ObjectStore = class {
39
21
  this.bucket = options.bucket;
40
22
  this.initialized = false;
41
23
  this.encryptor = encryption.encryptor;
42
- this.encryptionDisabled = encryption.disabled ?? false;
43
24
  }
25
+ openTelemetryCollector;
26
+ telemetryOptions;
44
27
  s3;
45
28
  bucket;
46
29
  initialized;
47
30
  encryptor;
48
- encryptionDisabled;
49
31
  // ---------------------------------------------------------------------------
50
- // Encryption helpers
32
+ // Encryption helpers — only active when compliance context is provided
51
33
  // ---------------------------------------------------------------------------
52
- encryptBody(body) {
53
- if (!this.encryptor || this.encryptionDisabled) return body;
54
- return this.encryptor.encrypt(body, getCurrentTenantId()) ?? body;
34
+ encryptBody(body, compliance) {
35
+ if (!compliance || !this.encryptor) return body;
36
+ return this.encryptor.encrypt(body, compliance.tenantId) ?? body;
55
37
  }
56
- decryptBody(body) {
57
- if (!this.encryptor || this.encryptionDisabled) return body;
38
+ decryptBody(body, compliance) {
39
+ if (!compliance || !this.encryptor) return body;
58
40
  if (!isEncrypted(body)) return body;
59
41
  try {
60
- return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;
42
+ return this.encryptor.decrypt(body, compliance.tenantId) ?? body;
61
43
  } catch {
62
44
  return body;
63
45
  }
@@ -73,20 +55,12 @@ var S3ObjectStore = class {
73
55
  }
74
56
  this.initialized = true;
75
57
  }
76
- /**
77
- * Stores an object in the S3 bucket.
78
- * @template T - The type of the object being stored.
79
- * @param object - The object to store. Must include a `key` property.
80
- *
81
- * @example
82
- * await store.putObject({ key: 'user-1', name: 'Alice' });
83
- */
84
- async putObject(object) {
58
+ async putObject(object, compliance) {
85
59
  if (!this.initialized) {
86
60
  await this.ensureBucketExists();
87
61
  }
88
62
  const { key, ...rest } = object;
89
- const body = this.encryptBody(JSON.stringify(rest));
63
+ const body = this.encryptBody(JSON.stringify(rest), compliance);
90
64
  const params = {
91
65
  Bucket: this.bucket,
92
66
  Key: key,
@@ -95,57 +69,20 @@ var S3ObjectStore = class {
95
69
  };
96
70
  await this.s3.send(new PutObjectCommand(params));
97
71
  }
98
- /**
99
- * Stores multiple objects in the S3 bucket.
100
- * @template T - The type of the objects being stored.
101
- * @param objects - The objects to store. Each must include a `key` property.
102
- *
103
- * @example
104
- * await store.putBatchObjects([
105
- * { key: 'user-1', name: 'Alice' },
106
- * { key: 'user-2', name: 'Bob' }
107
- * ]);
108
- */
109
- async putBatchObjects(objects) {
110
- await Promise.all(objects.map((obj) => this.putObject(obj)));
72
+ async putBatchObjects(objects, compliance) {
73
+ await Promise.all(objects.map((obj) => this.putObject(obj, compliance)));
111
74
  }
112
- /**
113
- * Streams an object upload to the S3 bucket.
114
- * For compatibility; uses putObject internally.
115
- * @template T - The type of the object being stored.
116
- * @param object - The object to stream-upload. Must include a `key` property.
117
- */
118
- async streamUploadObject(object) {
119
- await this.putObject(object);
75
+ async streamUploadObject(object, compliance) {
76
+ await this.putObject(object, compliance);
120
77
  }
121
- /**
122
- * Streams multiple object uploads to the S3 bucket.
123
- * For compatibility; uses putBatchObjects internally.
124
- * @template T - The type of the objects being stored.
125
- * @param objects - The objects to stream-upload. Each must include a `key` property.
126
- */
127
- async streamUploadBatchObjects(objects) {
128
- await this.putBatchObjects(objects);
78
+ async streamUploadBatchObjects(objects, compliance) {
79
+ await this.putBatchObjects(objects, compliance);
129
80
  }
130
- /**
131
- * Deletes an object from the S3 bucket.
132
- * @param objectKey - The key of the object to delete.
133
- *
134
- * @example
135
- * await store.deleteObject('user-1');
136
- */
137
81
  async deleteObject(objectKey) {
138
82
  await this.s3.send(
139
83
  new DeleteObjectCommand({ Bucket: this.bucket, Key: objectKey })
140
84
  );
141
85
  }
142
- /**
143
- * Deletes multiple objects from the S3 bucket.
144
- * @param objectKeys - The keys of the objects to delete.
145
- *
146
- * @example
147
- * await store.deleteBatchObjects(['user-1', 'user-2']);
148
- */
149
86
  async deleteBatchObjects(objectKeys) {
150
87
  const params = {
151
88
  Bucket: this.bucket,
@@ -155,16 +92,7 @@ var S3ObjectStore = class {
155
92
  };
156
93
  await this.s3.send(new DeleteObjectsCommand(params));
157
94
  }
158
- /**
159
- * Reads an object from the S3 bucket.
160
- * @template T - The expected type of the object.
161
- * @param objectKey - The key of the object to read.
162
- * @returns The parsed object.
163
- *
164
- * @example
165
- * const user = await store.readObject<{ name: string }>('user-1');
166
- */
167
- async readObject(objectKey) {
95
+ async readObject(objectKey, compliance) {
168
96
  const resp = await this.s3.send(
169
97
  new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
170
98
  );
@@ -172,32 +100,13 @@ var S3ObjectStore = class {
172
100
  throw new Error("S3 did not return a body");
173
101
  }
174
102
  const raw = await resp.Body.transformToString();
175
- return JSON.parse(this.decryptBody(raw));
103
+ return JSON.parse(this.decryptBody(raw, compliance));
176
104
  }
177
- /**
178
- * Reads multiple objects from the S3 bucket.
179
- * @template T - The expected type of the objects.
180
- * @param objectKeys - The keys of the objects to read.
181
- * @returns An array of parsed objects.
182
- *
183
- * @example
184
- * const users = await store.readBatchObjects<{ name: string }>(['user-1', 'user-2']);
185
- */
186
- async readBatchObjects(objectKeys) {
187
- return Promise.all(objectKeys.map((key) => this.readObject(key)));
105
+ async readBatchObjects(objectKeys, compliance) {
106
+ return Promise.all(
107
+ objectKeys.map((key) => this.readObject(key, compliance))
108
+ );
188
109
  }
189
- /**
190
- * Streams an object download from the S3 bucket.
191
- * Note: Streaming bypasses application-level encryption/decryption.
192
- * Use readObject for encrypted objects.
193
- * @param objectKey - The key of the object to download.
194
- * @returns A readable stream of the object's contents.
195
- * @throws If the S3 response does not include a readable stream.
196
- *
197
- * @example
198
- * const stream = await store.streamDownloadObject('user-1');
199
- * stream.pipe(fs.createWriteStream('user-1.json'));
200
- */
201
110
  async streamDownloadObject(objectKey) {
202
111
  const resp = await this.s3.send(
203
112
  new GetObjectCommand({ Bucket: this.bucket, Key: objectKey })
@@ -210,26 +119,9 @@ var S3ObjectStore = class {
210
119
  webStream
211
120
  );
212
121
  }
213
- /**
214
- * Streams multiple object downloads from the S3 bucket.
215
- * Note: Streaming bypasses application-level encryption/decryption.
216
- * @param objectKeys - The keys of the objects to download.
217
- * @returns An array of readable streams.
218
- *
219
- * @example
220
- * const streams = await store.streamDownloadBatchObjects(['user-1', 'user-2']);
221
- * streams[0].pipe(fs.createWriteStream('user-1.json'));
222
- */
223
122
  async streamDownloadBatchObjects(objectKeys) {
224
123
  return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));
225
124
  }
226
- /**
227
- * Gets the underlying S3 client instance.
228
- * @returns The S3Client instance used by this store.
229
- *
230
- * @example
231
- * const s3Client = store.getClient();
232
- */
233
125
  getClient() {
234
126
  return this.s3;
235
127
  }
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 {\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(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 { type FieldEncryptor } from '@forklaunch/core/persistence';\nimport { ObjectStore } from '@forklaunch/core/objectstore';\nimport type { ComplianceContext } from '@forklaunch/core/cache';\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}\n\n/**\n * Options for configuring the S3ObjectStore.\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 activated per-operation when a `compliance` context is provided.\n * Without it, object bodies are stored and read as plaintext.\n */\nexport class S3ObjectStore implements ObjectStore<S3Client> {\n private s3: S3Client;\n private bucket: string;\n private initialized: boolean;\n private encryptor?: FieldEncryptor;\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 }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers — only active when compliance context is provided\n // ---------------------------------------------------------------------------\n\n private encryptBody(body: string, compliance?: ComplianceContext): string {\n if (!compliance || !this.encryptor) return body;\n return this.encryptor.encrypt(body, compliance.tenantId) ?? body;\n }\n\n private decryptBody(body: string, compliance?: ComplianceContext): string {\n if (!compliance || !this.encryptor) return body;\n if (!isEncrypted(body)) return body;\n try {\n return this.encryptor.decrypt(body, compliance.tenantId) ?? 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 async putObject<T>(\n object: T & { key: string },\n compliance?: ComplianceContext\n ): 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), compliance);\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 async putBatchObjects<T>(\n objects: (T & { key: string })[],\n compliance?: ComplianceContext\n ): Promise<void> {\n await Promise.all(objects.map((obj) => this.putObject(obj, compliance)));\n }\n\n async streamUploadObject<T>(\n object: T & { key: string },\n compliance?: ComplianceContext\n ): Promise<void> {\n await this.putObject(object, compliance);\n }\n\n async streamUploadBatchObjects<T>(\n objects: (T & { key: string })[],\n compliance?: ComplianceContext\n ): Promise<void> {\n await this.putBatchObjects(objects, compliance);\n }\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 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 async readObject<T>(\n objectKey: string,\n compliance?: ComplianceContext\n ): 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, compliance)) as T;\n }\n\n async readBatchObjects<T>(\n objectKeys: string[],\n compliance?: ComplianceContext\n ): Promise<T[]> {\n return Promise.all(\n objectKeys.map((key) => this.readObject<T>(key, compliance))\n );\n }\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 async streamDownloadBatchObjects(objectKeys: string[]): Promise<Readable[]> {\n return Promise.all(objectKeys.map((key) => this.streamDownloadObject(key)));\n }\n\n getClient(): S3Client {\n return this.s3;\n }\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AASP,SAAS,gBAAgB;AAEzB,IAAM,qBAAqB,CAAC,OAAO,KAAK;AAExC,SAAS,YAAY,OAAwB;AAC3C,SAAO,mBAAmB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC;AAC3D;AA8BO,IAAM,gBAAN,MAAqD;AAAA,EAM1D,YACU,wBACR,SACQ,kBACR,YACA;AAJQ;AAEA;AAGR,SAAK,KAAK,QAAQ,UAAU,IAAI,SAAS,QAAQ,gBAAgB,CAAC,CAAC;AACnE,SAAK,SAAS,QAAQ;AACtB,SAAK,cAAc;AACnB,SAAK,YAAY,WAAW;AAAA,EAC9B;AAAA,EATU;AAAA,EAEA;AAAA,EARF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAkBA,YAAY,MAAc,YAAwC;AACxE,QAAI,CAAC,cAAc,CAAC,KAAK,UAAW,QAAO;AAC3C,WAAO,KAAK,UAAU,QAAQ,MAAM,WAAW,QAAQ,KAAK;AAAA,EAC9D;AAAA,EAEQ,YAAY,MAAc,YAAwC;AACxE,QAAI,CAAC,cAAc,CAAC,KAAK,UAAW,QAAO;AAC3C,QAAI,CAAC,YAAY,IAAI,EAAG,QAAO;AAC/B,QAAI;AACF,aAAO,KAAK,UAAU,QAAQ,MAAM,WAAW,QAAQ,KAAK;AAAA,IAC9D,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,qBAAqB;AACjC,QAAI;AACF,YAAM,KAAK,GAAG,KAAK,IAAI,kBAAkB,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC;AAAA,IACnE,QAAQ;AACN,YAAM,KAAK,GAAG,KAAK,IAAI,oBAAoB,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC;AAAA,IACrE;AAEA,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAM,UACJ,QACA,YACe;AACf,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,KAAK,mBAAmB;AAAA,IAChC;AAEA,UAAM,EAAE,KAAK,GAAG,KAAK,IAAI;AACzB,UAAM,OAAO,KAAK,YAAY,KAAK,UAAU,IAAI,GAAG,UAAU;AAC9D,UAAM,SAAgC;AAAA,MACpC,QAAQ,KAAK;AAAA,MACb,KAAK;AAAA,MACL,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AACA,UAAM,KAAK,GAAG,KAAK,IAAI,iBAAiB,MAAM,CAAC;AAAA,EACjD;AAAA,EAEA,MAAM,gBACJ,SACA,YACe;AACf,UAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC;AAAA,EACzE;AAAA,EAEA,MAAM,mBACJ,QACA,YACe;AACf,UAAM,KAAK,UAAU,QAAQ,UAAU;AAAA,EACzC;AAAA,EAEA,MAAM,yBACJ,SACA,YACe;AACf,UAAM,KAAK,gBAAgB,SAAS,UAAU;AAAA,EAChD;AAAA,EAEA,MAAM,aAAa,WAAkC;AACnD,UAAM,KAAK,GAAG;AAAA,MACZ,IAAI,oBAAoB,EAAE,QAAQ,KAAK,QAAQ,KAAK,UAAU,CAAC;AAAA,IACjE;AAAA,EACF;AAAA,EAEA,MAAM,mBAAmB,YAAqC;AAC5D,UAAM,SAAoC;AAAA,MACxC,QAAQ,KAAK;AAAA,MACb,QAAQ;AAAA,QACN,SAAS,WAAW,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE;AAAA,MAC5C;AAAA,IACF;AACA,UAAM,KAAK,GAAG,KAAK,IAAI,qBAAqB,MAAM,CAAC;AAAA,EACrD;AAAA,EAEA,MAAM,WACJ,WACA,YACY;AACZ,UAAM,OAAO,MAAM,KAAK,GAAG;AAAA,MACzB,IAAI,iBAAiB,EAAE,QAAQ,KAAK,QAAQ,KAAK,UAAU,CAAC;AAAA,IAC9D;AAEA,QAAI,CAAC,KAAK,MAAM;AACd,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAEA,UAAM,MAAM,MAAM,KAAK,KAAK,kBAAkB;AAC9C,WAAO,KAAK,MAAM,KAAK,YAAY,KAAK,UAAU,CAAC;AAAA,EACrD;AAAA,EAEA,MAAM,iBACJ,YACA,YACc;AACd,WAAO,QAAQ;AAAA,MACb,WAAW,IAAI,CAAC,QAAQ,KAAK,WAAc,KAAK,UAAU,CAAC;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,WAAsC;AAC/D,UAAM,OAAO,MAAM,KAAK,GAAG;AAAA,MACzB,IAAI,iBAAiB,EAAE,QAAQ,KAAK,QAAQ,KAAK,UAAU,CAAC;AAAA,IAC9D;AACA,UAAM,YAAY,KAAK,MAAM,qBAAqB;AAClD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,4BAA4B;AAAA,IAC9C;AAEA,WAAO,SAAS;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,2BAA2B,YAA2C;AAC1E,WAAO,QAAQ,IAAI,WAAW,IAAI,CAAC,QAAQ,KAAK,qBAAqB,GAAG,CAAC,CAAC;AAAA,EAC5E;AAAA,EAEA,YAAsB;AACpB,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forklaunch/infrastructure-s3",
3
- "version": "1.3.14",
3
+ "version": "1.4.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.1020.0",
32
- "@forklaunch/common": "1.2.13",
33
- "@forklaunch/core": "1.3.16"
32
+ "@forklaunch/common": "1.2.14",
33
+ "@forklaunch/core": "1.4.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/js": "^10.0.1",