@constructive-io/bucket-provisioner 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +335 -0
- package/client.d.ts +19 -0
- package/client.js +45 -0
- package/cors.d.ts +33 -0
- package/cors.js +88 -0
- package/esm/client.d.ts +19 -0
- package/esm/client.js +42 -0
- package/esm/cors.d.ts +33 -0
- package/esm/cors.js +84 -0
- package/esm/index.d.ts +37 -0
- package/esm/index.js +39 -0
- package/esm/lifecycle.d.ts +29 -0
- package/esm/lifecycle.js +42 -0
- package/esm/policies.d.ts +88 -0
- package/esm/policies.js +121 -0
- package/esm/provisioner.d.ts +137 -0
- package/esm/provisioner.js +397 -0
- package/esm/types.d.ts +155 -0
- package/esm/types.js +19 -0
- package/index.d.ts +37 -0
- package/index.js +53 -0
- package/lifecycle.d.ts +29 -0
- package/lifecycle.js +46 -0
- package/package.json +47 -0
- package/policies.d.ts +88 -0
- package/policies.js +127 -0
- package/provisioner.d.ts +137 -0
- package/provisioner.js +401 -0
- package/types.d.ts +155 -0
- package/types.js +23 -0
package/provisioner.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bucket Provisioner — core provisioning logic.
|
|
4
|
+
*
|
|
5
|
+
* Orchestrates S3 bucket creation, privacy configuration, CORS setup,
|
|
6
|
+
* versioning, and lifecycle rules. Uses the AWS SDK S3 client for all
|
|
7
|
+
* operations, which works with any S3-compatible backend (MinIO, R2, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Privacy model:
|
|
10
|
+
* - Private/temp buckets: Block All Public Access, no bucket policy, presigned URLs only
|
|
11
|
+
* - Public buckets: Block Public Access partially relaxed, public-read bucket policy applied
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.BucketProvisioner = void 0;
|
|
15
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const client_1 = require("./client");
|
|
18
|
+
const policies_1 = require("./policies");
|
|
19
|
+
const cors_1 = require("./cors");
|
|
20
|
+
const lifecycle_1 = require("./lifecycle");
|
|
21
|
+
/**
|
|
22
|
+
* The BucketProvisioner handles creating and configuring S3-compatible
|
|
23
|
+
* buckets with the correct privacy settings, CORS rules, and policies
|
|
24
|
+
* for the Constructive storage module.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const provisioner = new BucketProvisioner({
|
|
29
|
+
* connection: {
|
|
30
|
+
* provider: 'minio',
|
|
31
|
+
* region: 'us-east-1',
|
|
32
|
+
* endpoint: 'http://minio:9000',
|
|
33
|
+
* accessKeyId: 'minioadmin',
|
|
34
|
+
* secretAccessKey: 'minioadmin',
|
|
35
|
+
* },
|
|
36
|
+
* allowedOrigins: ['https://app.example.com'],
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Provision a private bucket
|
|
40
|
+
* const result = await provisioner.provision({
|
|
41
|
+
* bucketName: 'my-app-storage',
|
|
42
|
+
* accessType: 'private',
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
class BucketProvisioner {
|
|
47
|
+
client;
|
|
48
|
+
config;
|
|
49
|
+
allowedOrigins;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
if (!options.allowedOrigins || options.allowedOrigins.length === 0) {
|
|
52
|
+
throw new types_1.ProvisionerError('INVALID_CONFIG', 'allowedOrigins must contain at least one origin for CORS configuration');
|
|
53
|
+
}
|
|
54
|
+
this.config = options.connection;
|
|
55
|
+
this.allowedOrigins = options.allowedOrigins;
|
|
56
|
+
this.client = (0, client_1.createS3Client)(options.connection);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the underlying S3Client instance.
|
|
60
|
+
* Useful for advanced operations not covered by the provisioner.
|
|
61
|
+
*/
|
|
62
|
+
getClient() {
|
|
63
|
+
return this.client;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Provision a fully configured S3 bucket.
|
|
67
|
+
*
|
|
68
|
+
* This is the main entry point. It:
|
|
69
|
+
* 1. Creates the bucket (or verifies it exists)
|
|
70
|
+
* 2. Configures Block Public Access based on access type
|
|
71
|
+
* 3. Applies the appropriate bucket policy (public-read or none)
|
|
72
|
+
* 4. Sets CORS rules for presigned URL uploads
|
|
73
|
+
* 5. Optionally enables versioning
|
|
74
|
+
* 6. Optionally adds lifecycle rules (auto-enabled for temp buckets)
|
|
75
|
+
*
|
|
76
|
+
* @param options - Bucket creation options
|
|
77
|
+
* @returns ProvisionResult with all configuration details
|
|
78
|
+
*/
|
|
79
|
+
async provision(options) {
|
|
80
|
+
const { bucketName, accessType, versioning = false } = options;
|
|
81
|
+
const region = options.region ?? this.config.region;
|
|
82
|
+
// 1. Create the bucket
|
|
83
|
+
await this.createBucket(bucketName, region);
|
|
84
|
+
// 2. Configure Block Public Access
|
|
85
|
+
const publicAccessBlock = (0, policies_1.getPublicAccessBlock)(accessType);
|
|
86
|
+
await this.setPublicAccessBlock(bucketName, publicAccessBlock);
|
|
87
|
+
// 3. Apply bucket policy
|
|
88
|
+
if (accessType === 'public') {
|
|
89
|
+
const policy = (0, policies_1.buildPublicReadPolicy)(bucketName);
|
|
90
|
+
await this.setBucketPolicy(bucketName, policy);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Ensure no leftover public policy on private/temp buckets
|
|
94
|
+
await this.deleteBucketPolicy(bucketName);
|
|
95
|
+
}
|
|
96
|
+
// 4. Set CORS rules (per-bucket override takes precedence over default)
|
|
97
|
+
const effectiveOrigins = options.allowedOrigins ?? this.allowedOrigins;
|
|
98
|
+
const corsRules = accessType === 'private'
|
|
99
|
+
? (0, cors_1.buildPrivateCorsRules)(effectiveOrigins)
|
|
100
|
+
: (0, cors_1.buildUploadCorsRules)(effectiveOrigins);
|
|
101
|
+
await this.setCors(bucketName, corsRules);
|
|
102
|
+
// 5. Versioning
|
|
103
|
+
if (versioning) {
|
|
104
|
+
await this.enableVersioning(bucketName);
|
|
105
|
+
}
|
|
106
|
+
// 6. Lifecycle rules for temp buckets
|
|
107
|
+
const lifecycleRules = [];
|
|
108
|
+
if (accessType === 'temp') {
|
|
109
|
+
const tempRule = (0, lifecycle_1.buildTempCleanupRule)(1);
|
|
110
|
+
lifecycleRules.push(tempRule);
|
|
111
|
+
await this.setLifecycleRules(bucketName, lifecycleRules);
|
|
112
|
+
}
|
|
113
|
+
// Build result
|
|
114
|
+
const publicUrlPrefix = accessType === 'public'
|
|
115
|
+
? (options.publicUrlPrefix ?? null)
|
|
116
|
+
: null;
|
|
117
|
+
return {
|
|
118
|
+
bucketName,
|
|
119
|
+
accessType,
|
|
120
|
+
endpoint: this.config.endpoint ?? null,
|
|
121
|
+
provider: this.config.provider,
|
|
122
|
+
region,
|
|
123
|
+
publicUrlPrefix,
|
|
124
|
+
blockPublicAccess: accessType !== 'public',
|
|
125
|
+
versioning,
|
|
126
|
+
corsRules,
|
|
127
|
+
lifecycleRules,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create an S3 bucket. Handles the "bucket already exists" case gracefully.
|
|
132
|
+
*/
|
|
133
|
+
async createBucket(bucketName, region) {
|
|
134
|
+
try {
|
|
135
|
+
const command = new client_s3_1.CreateBucketCommand({
|
|
136
|
+
Bucket: bucketName,
|
|
137
|
+
...(region && region !== 'us-east-1'
|
|
138
|
+
? { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
139
|
+
: {}),
|
|
140
|
+
});
|
|
141
|
+
await this.client.send(command);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// Bucket already exists and we own it — that's fine
|
|
145
|
+
if (err.name === 'BucketAlreadyOwnedByYou' ||
|
|
146
|
+
err.name === 'BucketAlreadyExists' ||
|
|
147
|
+
err.Code === 'BucketAlreadyOwnedByYou' ||
|
|
148
|
+
err.Code === 'BucketAlreadyExists') {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw new types_1.ProvisionerError('PROVIDER_ERROR', `Failed to create bucket '${bucketName}': ${err.message}`, err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check if a bucket exists and is accessible.
|
|
156
|
+
*/
|
|
157
|
+
async bucketExists(bucketName) {
|
|
158
|
+
try {
|
|
159
|
+
await this.client.send(new client_s3_1.HeadBucketCommand({ Bucket: bucketName }));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (err.$metadata?.httpStatusCode === 403) {
|
|
167
|
+
throw new types_1.ProvisionerError('ACCESS_DENIED', `Access denied to bucket '${bucketName}'`, err);
|
|
168
|
+
}
|
|
169
|
+
throw new types_1.ProvisionerError('PROVIDER_ERROR', `Failed to check bucket '${bucketName}': ${err.message}`, err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Configure S3 Block Public Access settings.
|
|
174
|
+
*/
|
|
175
|
+
async setPublicAccessBlock(bucketName, config) {
|
|
176
|
+
try {
|
|
177
|
+
await this.client.send(new client_s3_1.PutPublicAccessBlockCommand({
|
|
178
|
+
Bucket: bucketName,
|
|
179
|
+
PublicAccessBlockConfiguration: config,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set public access block on '${bucketName}': ${err.message}`, err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Apply an S3 bucket policy.
|
|
188
|
+
*/
|
|
189
|
+
async setBucketPolicy(bucketName, policy) {
|
|
190
|
+
try {
|
|
191
|
+
await this.client.send(new client_s3_1.PutBucketPolicyCommand({
|
|
192
|
+
Bucket: bucketName,
|
|
193
|
+
Policy: JSON.stringify(policy),
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set bucket policy on '${bucketName}': ${err.message}`, err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Delete an S3 bucket policy (used to clear leftover public policies).
|
|
202
|
+
*/
|
|
203
|
+
async deleteBucketPolicy(bucketName) {
|
|
204
|
+
try {
|
|
205
|
+
await this.client.send(new client_s3_1.DeleteBucketPolicyCommand({ Bucket: bucketName }));
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// No policy to delete — that's fine
|
|
209
|
+
if (err.name === 'NoSuchBucketPolicy' || err.$metadata?.httpStatusCode === 404) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to delete bucket policy on '${bucketName}': ${err.message}`, err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Set CORS configuration on an S3 bucket.
|
|
217
|
+
*/
|
|
218
|
+
async setCors(bucketName, rules) {
|
|
219
|
+
try {
|
|
220
|
+
await this.client.send(new client_s3_1.PutBucketCorsCommand({
|
|
221
|
+
Bucket: bucketName,
|
|
222
|
+
CORSConfiguration: {
|
|
223
|
+
CORSRules: rules.map((rule) => ({
|
|
224
|
+
AllowedOrigins: rule.allowedOrigins,
|
|
225
|
+
AllowedMethods: rule.allowedMethods,
|
|
226
|
+
AllowedHeaders: rule.allowedHeaders,
|
|
227
|
+
ExposeHeaders: rule.exposedHeaders,
|
|
228
|
+
MaxAgeSeconds: rule.maxAgeSeconds,
|
|
229
|
+
})),
|
|
230
|
+
},
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
throw new types_1.ProvisionerError('CORS_FAILED', `Failed to set CORS on '${bucketName}': ${err.message}`, err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Enable versioning on an S3 bucket.
|
|
239
|
+
*/
|
|
240
|
+
async enableVersioning(bucketName) {
|
|
241
|
+
try {
|
|
242
|
+
await this.client.send(new client_s3_1.PutBucketVersioningCommand({
|
|
243
|
+
Bucket: bucketName,
|
|
244
|
+
VersioningConfiguration: { Status: 'Enabled' },
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
throw new types_1.ProvisionerError('VERSIONING_FAILED', `Failed to enable versioning on '${bucketName}': ${err.message}`, err);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Set lifecycle rules on an S3 bucket.
|
|
253
|
+
*/
|
|
254
|
+
async setLifecycleRules(bucketName, rules) {
|
|
255
|
+
try {
|
|
256
|
+
await this.client.send(new client_s3_1.PutBucketLifecycleConfigurationCommand({
|
|
257
|
+
Bucket: bucketName,
|
|
258
|
+
LifecycleConfiguration: {
|
|
259
|
+
Rules: rules.map((rule) => ({
|
|
260
|
+
ID: rule.id,
|
|
261
|
+
Filter: { Prefix: rule.prefix },
|
|
262
|
+
Status: rule.enabled ? 'Enabled' : 'Disabled',
|
|
263
|
+
Expiration: { Days: rule.expirationDays },
|
|
264
|
+
})),
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
throw new types_1.ProvisionerError('LIFECYCLE_FAILED', `Failed to set lifecycle rules on '${bucketName}': ${err.message}`, err);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Update CORS configuration on an existing S3 bucket.
|
|
274
|
+
*
|
|
275
|
+
* Call this when the `allowed_origins` column changes on a bucket row.
|
|
276
|
+
* Builds the appropriate CORS rule set for the bucket's access type
|
|
277
|
+
* and applies it to the S3 bucket.
|
|
278
|
+
*
|
|
279
|
+
* @param options - Bucket name, access type, and new allowed origins
|
|
280
|
+
* @returns The CORS rules that were applied
|
|
281
|
+
*/
|
|
282
|
+
async updateCors(options) {
|
|
283
|
+
const { bucketName, accessType, allowedOrigins } = options;
|
|
284
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
285
|
+
throw new types_1.ProvisionerError('INVALID_CONFIG', 'allowedOrigins must contain at least one origin for CORS configuration');
|
|
286
|
+
}
|
|
287
|
+
const corsRules = accessType === 'private'
|
|
288
|
+
? (0, cors_1.buildPrivateCorsRules)(allowedOrigins)
|
|
289
|
+
: (0, cors_1.buildUploadCorsRules)(allowedOrigins);
|
|
290
|
+
await this.setCors(bucketName, corsRules);
|
|
291
|
+
return corsRules;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Inspect the current configuration of an existing bucket.
|
|
295
|
+
*
|
|
296
|
+
* Reads the bucket's policy, CORS, versioning, lifecycle, and public access
|
|
297
|
+
* settings and returns them in a structured format. Useful for auditing
|
|
298
|
+
* or verifying that a bucket is correctly configured.
|
|
299
|
+
*
|
|
300
|
+
* @param bucketName - S3 bucket name
|
|
301
|
+
* @param accessType - Expected access type (used in the result)
|
|
302
|
+
*/
|
|
303
|
+
async inspect(bucketName, accessType) {
|
|
304
|
+
const exists = await this.bucketExists(bucketName);
|
|
305
|
+
if (!exists) {
|
|
306
|
+
throw new types_1.ProvisionerError('BUCKET_NOT_FOUND', `Bucket '${bucketName}' does not exist`);
|
|
307
|
+
}
|
|
308
|
+
// Read all configurations in parallel
|
|
309
|
+
const [publicAccessBlock, policy, cors, versioning, lifecycle] = await Promise.all([
|
|
310
|
+
this.getPublicAccessBlock(bucketName),
|
|
311
|
+
this.getBucketPolicy(bucketName),
|
|
312
|
+
this.getBucketCors(bucketName),
|
|
313
|
+
this.getBucketVersioning(bucketName),
|
|
314
|
+
this.getBucketLifecycle(bucketName),
|
|
315
|
+
]);
|
|
316
|
+
const isFullyBlocked = publicAccessBlock
|
|
317
|
+
? publicAccessBlock.BlockPublicAcls === true &&
|
|
318
|
+
publicAccessBlock.IgnorePublicAcls === true &&
|
|
319
|
+
publicAccessBlock.BlockPublicPolicy === true &&
|
|
320
|
+
publicAccessBlock.RestrictPublicBuckets === true
|
|
321
|
+
: false;
|
|
322
|
+
return {
|
|
323
|
+
bucketName,
|
|
324
|
+
accessType,
|
|
325
|
+
endpoint: this.config.endpoint ?? null,
|
|
326
|
+
provider: this.config.provider,
|
|
327
|
+
region: this.config.region,
|
|
328
|
+
publicUrlPrefix: null,
|
|
329
|
+
blockPublicAccess: isFullyBlocked,
|
|
330
|
+
versioning: versioning === 'Enabled',
|
|
331
|
+
corsRules: cors,
|
|
332
|
+
lifecycleRules: lifecycle,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
// --- Private read methods for inspect ---
|
|
336
|
+
async getPublicAccessBlock(bucketName) {
|
|
337
|
+
try {
|
|
338
|
+
const result = await this.client.send(new client_s3_1.GetPublicAccessBlockCommand({ Bucket: bucketName }));
|
|
339
|
+
const config = result.PublicAccessBlockConfiguration;
|
|
340
|
+
if (!config)
|
|
341
|
+
return null;
|
|
342
|
+
return {
|
|
343
|
+
BlockPublicAcls: config.BlockPublicAcls ?? false,
|
|
344
|
+
IgnorePublicAcls: config.IgnorePublicAcls ?? false,
|
|
345
|
+
BlockPublicPolicy: config.BlockPublicPolicy ?? false,
|
|
346
|
+
RestrictPublicBuckets: config.RestrictPublicBuckets ?? false,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async getBucketPolicy(bucketName) {
|
|
354
|
+
try {
|
|
355
|
+
const result = await this.client.send(new client_s3_1.GetBucketPolicyCommand({ Bucket: bucketName }));
|
|
356
|
+
return result.Policy ? JSON.parse(result.Policy) : null;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async getBucketCors(bucketName) {
|
|
363
|
+
try {
|
|
364
|
+
const result = await this.client.send(new client_s3_1.GetBucketCorsCommand({ Bucket: bucketName }));
|
|
365
|
+
return (result.CORSRules ?? []).map((rule) => ({
|
|
366
|
+
allowedOrigins: rule.AllowedOrigins ?? [],
|
|
367
|
+
allowedMethods: (rule.AllowedMethods ?? []),
|
|
368
|
+
allowedHeaders: rule.AllowedHeaders ?? [],
|
|
369
|
+
exposedHeaders: rule.ExposeHeaders ?? [],
|
|
370
|
+
maxAgeSeconds: rule.MaxAgeSeconds ?? 0,
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async getBucketVersioning(bucketName) {
|
|
378
|
+
try {
|
|
379
|
+
const result = await this.client.send(new client_s3_1.GetBucketVersioningCommand({ Bucket: bucketName }));
|
|
380
|
+
return result.Status ?? 'Disabled';
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return 'Disabled';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async getBucketLifecycle(bucketName) {
|
|
387
|
+
try {
|
|
388
|
+
const result = await this.client.send(new client_s3_1.GetBucketLifecycleConfigurationCommand({ Bucket: bucketName }));
|
|
389
|
+
return (result.Rules ?? []).map((rule) => ({
|
|
390
|
+
id: rule.ID ?? '',
|
|
391
|
+
prefix: rule.Filter?.Prefix ?? '',
|
|
392
|
+
expirationDays: rule.Expiration?.Days ?? 0,
|
|
393
|
+
enabled: rule.Status === 'Enabled',
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.BucketProvisioner = BucketProvisioner;
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the bucket provisioner library.
|
|
3
|
+
*
|
|
4
|
+
* Defines the configuration interfaces for S3-compatible storage providers,
|
|
5
|
+
* bucket creation options, privacy policies, and CORS rules.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Supported storage provider identifiers.
|
|
9
|
+
*
|
|
10
|
+
* Used to select provider-specific behavior (e.g., path-style URLs for MinIO,
|
|
11
|
+
* jurisdiction headers for R2).
|
|
12
|
+
*/
|
|
13
|
+
export type StorageProvider = 's3' | 'minio' | 'r2' | 'gcs' | 'spaces';
|
|
14
|
+
/**
|
|
15
|
+
* Connection configuration for an S3-compatible storage backend.
|
|
16
|
+
*
|
|
17
|
+
* This is the input you provide to connect to your storage provider.
|
|
18
|
+
* For AWS S3, only `region` and credentials are needed.
|
|
19
|
+
* For MinIO/R2/etc., also provide `endpoint`.
|
|
20
|
+
*/
|
|
21
|
+
export interface StorageConnectionConfig {
|
|
22
|
+
/** Storage provider type */
|
|
23
|
+
provider: StorageProvider;
|
|
24
|
+
/** S3 region (e.g., "us-east-1"). Required for AWS S3. */
|
|
25
|
+
region: string;
|
|
26
|
+
/** S3-compatible endpoint URL (e.g., "http://minio:9000"). Required for non-AWS providers. */
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
/** AWS access key ID */
|
|
29
|
+
accessKeyId: string;
|
|
30
|
+
/** AWS secret access key */
|
|
31
|
+
secretAccessKey: string;
|
|
32
|
+
/** Use path-style URLs (required for MinIO, optional for others) */
|
|
33
|
+
forcePathStyle?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Bucket access type, matching the database bucket `type` column.
|
|
37
|
+
*
|
|
38
|
+
* - `public`: Files served via CDN/public URL. Bucket policy allows public reads.
|
|
39
|
+
* - `private`: Files served via presigned GET URLs only. No public access.
|
|
40
|
+
* - `temp`: Staging area for uploads. Treated as private. Lifecycle rules may apply.
|
|
41
|
+
*/
|
|
42
|
+
export type BucketAccessType = 'public' | 'private' | 'temp';
|
|
43
|
+
/**
|
|
44
|
+
* Options for creating or configuring an S3 bucket.
|
|
45
|
+
*/
|
|
46
|
+
export interface CreateBucketOptions {
|
|
47
|
+
/** The S3 bucket name (globally unique for AWS, locally unique for MinIO) */
|
|
48
|
+
bucketName: string;
|
|
49
|
+
/** Bucket access type — determines which policies are applied */
|
|
50
|
+
accessType: BucketAccessType;
|
|
51
|
+
/** S3 region for bucket creation (defaults to connection config region) */
|
|
52
|
+
region?: string;
|
|
53
|
+
/** Whether to enable versioning (recommended for durability) */
|
|
54
|
+
versioning?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Public URL prefix for public buckets.
|
|
57
|
+
* This is the CDN or public endpoint URL that serves files from the bucket.
|
|
58
|
+
* Only meaningful for `accessType: 'public'`.
|
|
59
|
+
* Example: "https://cdn.example.com/public"
|
|
60
|
+
*/
|
|
61
|
+
publicUrlPrefix?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Per-bucket CORS allowed origins override.
|
|
64
|
+
* When provided, these origins are used instead of the provisioner's default allowedOrigins.
|
|
65
|
+
* Use ['*'] for open/CDN mode (wildcard CORS, any origin can fetch).
|
|
66
|
+
* NULL/undefined = use the provisioner's default allowedOrigins.
|
|
67
|
+
*/
|
|
68
|
+
allowedOrigins?: string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Options for updating CORS on an existing S3 bucket.
|
|
72
|
+
*/
|
|
73
|
+
export interface UpdateCorsOptions {
|
|
74
|
+
/** The S3 bucket name */
|
|
75
|
+
bucketName: string;
|
|
76
|
+
/** Bucket access type — determines which CORS rule set to apply */
|
|
77
|
+
accessType: BucketAccessType;
|
|
78
|
+
/** The allowed origins to set. Use ['*'] for open/CDN mode. */
|
|
79
|
+
allowedOrigins: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* CORS rule for S3 bucket configuration.
|
|
83
|
+
*
|
|
84
|
+
* Required for browser-based presigned URL uploads.
|
|
85
|
+
* The presigned PUT request is a cross-origin request from the client
|
|
86
|
+
* to the S3 endpoint, so CORS must be configured on the bucket.
|
|
87
|
+
*/
|
|
88
|
+
export interface CorsRule {
|
|
89
|
+
/** Allowed origin domains (e.g., ["https://app.example.com"]) */
|
|
90
|
+
allowedOrigins: string[];
|
|
91
|
+
/** Allowed HTTP methods */
|
|
92
|
+
allowedMethods: ('GET' | 'PUT' | 'HEAD' | 'POST' | 'DELETE')[];
|
|
93
|
+
/** Allowed request headers */
|
|
94
|
+
allowedHeaders: string[];
|
|
95
|
+
/** Headers exposed to the browser */
|
|
96
|
+
exposedHeaders: string[];
|
|
97
|
+
/** Preflight cache duration in seconds */
|
|
98
|
+
maxAgeSeconds: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lifecycle rule for automatic object expiration.
|
|
102
|
+
*
|
|
103
|
+
* Useful for temp buckets where uploads expire after a set period.
|
|
104
|
+
*/
|
|
105
|
+
export interface LifecycleRule {
|
|
106
|
+
/** Rule ID (descriptive name) */
|
|
107
|
+
id: string;
|
|
108
|
+
/** S3 key prefix to apply the rule to (empty string = entire bucket) */
|
|
109
|
+
prefix: string;
|
|
110
|
+
/** Number of days after which objects expire */
|
|
111
|
+
expirationDays: number;
|
|
112
|
+
/** Whether the rule is enabled */
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Result of a bucket provisioning operation.
|
|
117
|
+
*
|
|
118
|
+
* Contains all the information needed to configure the `storage_module`
|
|
119
|
+
* table and the presigned URL plugin.
|
|
120
|
+
*/
|
|
121
|
+
export interface ProvisionResult {
|
|
122
|
+
/** The S3 bucket name */
|
|
123
|
+
bucketName: string;
|
|
124
|
+
/** Bucket access type */
|
|
125
|
+
accessType: BucketAccessType;
|
|
126
|
+
/** S3 endpoint URL (null for AWS S3 default) */
|
|
127
|
+
endpoint: string | null;
|
|
128
|
+
/** Storage provider type */
|
|
129
|
+
provider: StorageProvider;
|
|
130
|
+
/** S3 region */
|
|
131
|
+
region: string;
|
|
132
|
+
/**
|
|
133
|
+
* Public URL prefix for download URLs.
|
|
134
|
+
* For public buckets: the CDN/public endpoint.
|
|
135
|
+
* For private buckets: null (presigned URLs only).
|
|
136
|
+
*/
|
|
137
|
+
publicUrlPrefix: string | null;
|
|
138
|
+
/** Whether Block Public Access is enabled */
|
|
139
|
+
blockPublicAccess: boolean;
|
|
140
|
+
/** Whether versioning is enabled */
|
|
141
|
+
versioning: boolean;
|
|
142
|
+
/** CORS rules applied */
|
|
143
|
+
corsRules: CorsRule[];
|
|
144
|
+
/** Lifecycle rules applied */
|
|
145
|
+
lifecycleRules: LifecycleRule[];
|
|
146
|
+
}
|
|
147
|
+
export type ProvisionerErrorCode = 'CONNECTION_FAILED' | 'BUCKET_ALREADY_EXISTS' | 'BUCKET_NOT_FOUND' | 'INVALID_CONFIG' | 'POLICY_FAILED' | 'CORS_FAILED' | 'LIFECYCLE_FAILED' | 'VERSIONING_FAILED' | 'ACCESS_DENIED' | 'PROVIDER_ERROR';
|
|
148
|
+
/**
|
|
149
|
+
* Structured error thrown by the bucket provisioner.
|
|
150
|
+
*/
|
|
151
|
+
export declare class ProvisionerError extends Error {
|
|
152
|
+
readonly code: ProvisionerErrorCode;
|
|
153
|
+
readonly cause?: unknown;
|
|
154
|
+
constructor(code: ProvisionerErrorCode, message: string, cause?: unknown);
|
|
155
|
+
}
|
package/types.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Types for the bucket provisioner library.
|
|
4
|
+
*
|
|
5
|
+
* Defines the configuration interfaces for S3-compatible storage providers,
|
|
6
|
+
* bucket creation options, privacy policies, and CORS rules.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ProvisionerError = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Structured error thrown by the bucket provisioner.
|
|
12
|
+
*/
|
|
13
|
+
class ProvisionerError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
cause;
|
|
16
|
+
constructor(code, message, cause) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ProvisionerError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.ProvisionerError = ProvisionerError;
|