@codercito/object-storage 0.1.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 +21 -0
- package/README.md +285 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# @codercito/object-storage
|
|
2
|
+
|
|
3
|
+
Abstracción tipada para object storage compatible con S3 (Amazon S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2) con helpers para construir URLs públicas via CDN.
|
|
4
|
+
|
|
5
|
+
- ✅ Subida directa, `exists`, y presigned POST listos para uso en backend.
|
|
6
|
+
- ✅ Tipado estricto con TypeScript, sin `any`.
|
|
7
|
+
- ✅ Cero dependencias propias. El AWS SDK es `peerDependency` (no se duplica).
|
|
8
|
+
- ✅ Sin logs internos: tú decides cómo loggear vía callback `onError`.
|
|
9
|
+
- ✅ Diseñado para abrirse a otros providers manteniendo la misma interfaz.
|
|
10
|
+
|
|
11
|
+
## Instalación
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @codercito/object-storage @aws-sdk/client-s3 @aws-sdk/s3-presigned-post
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Los paquetes `@aws-sdk/*` son `peerDependencies`: la librería los espera instalados pero no los incluye en su bundle.
|
|
18
|
+
|
|
19
|
+
## Uso básico
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { S3StorageService, CdnFileUrlService } from '@codercito/object-storage';
|
|
23
|
+
|
|
24
|
+
// Configura el servicio - el consumidor decide de dónde vienen los valores
|
|
25
|
+
const storage = new S3StorageService({
|
|
26
|
+
bucket: process.env.S3_BUCKET!,
|
|
27
|
+
region: process.env.S3_REGION!,
|
|
28
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
29
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const urls = new CdnFileUrlService(process.env.CDN_PUBLIC_URL!);
|
|
33
|
+
|
|
34
|
+
// Subir un archivo
|
|
35
|
+
await storage.put('avatars/user-123.jpg', buffer, {
|
|
36
|
+
contentType: 'image/jpeg',
|
|
37
|
+
cacheControl: 'public, max-age=31536000',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Verificar existencia
|
|
41
|
+
const exists = await storage.exists('avatars/user-123.jpg');
|
|
42
|
+
|
|
43
|
+
// Construir URL pública
|
|
44
|
+
const url = urls.getPublicUrl('avatars/user-123.jpg');
|
|
45
|
+
// → https://cdn.miapp.com/avatars/user-123.jpg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Subida directa desde el cliente (presigned POST)
|
|
49
|
+
|
|
50
|
+
Genera credenciales en el backend para que el cliente suba directamente al storage sin pasar bytes por tu servidor:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Backend (ej. API route de Next.js)
|
|
54
|
+
const presigned = await storage.presignedPost({
|
|
55
|
+
key: `uploads/${userId}/${filename}`,
|
|
56
|
+
contentType: 'image/jpeg',
|
|
57
|
+
maxSizeBytes: 5 * 1024 * 1024, // 5 MB
|
|
58
|
+
expiresIn: 600, // 10 minutos
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Devuélvelo al cliente
|
|
62
|
+
return Response.json(presigned);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// Cliente (browser)
|
|
67
|
+
const formData = new FormData();
|
|
68
|
+
Object.entries(presigned.fields).forEach(([k, v]) => formData.append(k, v));
|
|
69
|
+
formData.append('file', file);
|
|
70
|
+
|
|
71
|
+
await fetch(presigned.url, { method: 'POST', body: formData });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Providers compatibles
|
|
75
|
+
|
|
76
|
+
`S3StorageService` funciona con cualquier API S3-compatible cambiando el `endpoint`:
|
|
77
|
+
|
|
78
|
+
### Cloudflare R2
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const storage = new S3StorageService({
|
|
82
|
+
bucket: 'my-bucket',
|
|
83
|
+
region: 'auto',
|
|
84
|
+
endpoint: 'https://ACCOUNT_ID.r2.cloudflarestorage.com',
|
|
85
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
86
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### MinIO (local development)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const storage = new S3StorageService({
|
|
94
|
+
bucket: 'my-bucket',
|
|
95
|
+
region: 'us-east-1',
|
|
96
|
+
endpoint: 'http://localhost:9000',
|
|
97
|
+
accessKeyId: 'minioadmin',
|
|
98
|
+
secretAccessKey: 'minioadmin',
|
|
99
|
+
forcePathStyle: true, // necesario para MinIO
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### DigitalOcean Spaces
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const storage = new S3StorageService({
|
|
107
|
+
bucket: 'my-space',
|
|
108
|
+
region: 'nyc3',
|
|
109
|
+
endpoint: 'https://nyc3.digitaloceanspaces.com',
|
|
110
|
+
accessKeyId: process.env.DO_SPACES_KEY!,
|
|
111
|
+
secretAccessKey: process.env.DO_SPACES_SECRET!,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Manejo de errores
|
|
116
|
+
|
|
117
|
+
La librería expone tres clases de error:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import {
|
|
121
|
+
StorageUploadError,
|
|
122
|
+
StoragePresignError,
|
|
123
|
+
StorageConfigError,
|
|
124
|
+
} from '@codercito/object-storage';
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await storage.put('a.jpg', buffer, { contentType: 'image/jpeg' });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e instanceof StorageUploadError) {
|
|
130
|
+
console.error('Falló la subida', e.key, e.cause);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`StorageConfigError` se lanza al construir el servicio si falta algún campo obligatorio.
|
|
136
|
+
|
|
137
|
+
### Logging y observabilidad
|
|
138
|
+
|
|
139
|
+
La librería **no escribe a `console`** por sí sola. Si quieres loggear los errores internos (antes de que se lancen), inyecta un callback `onError`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import pino from 'pino';
|
|
143
|
+
const logger = pino();
|
|
144
|
+
|
|
145
|
+
const storage = new S3StorageService({
|
|
146
|
+
bucket: '...', region: '...',
|
|
147
|
+
accessKeyId: '...', secretAccessKey: '...',
|
|
148
|
+
onError: (event) => {
|
|
149
|
+
logger.error({
|
|
150
|
+
operation: event.operation,
|
|
151
|
+
key: event.key,
|
|
152
|
+
cause: event.cause,
|
|
153
|
+
}, 'Storage error');
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Esto te permite integrar la librería con Pino, Winston, Sentry, Datadog, etc. sin que la librería decida por ti.
|
|
159
|
+
|
|
160
|
+
## Validar variables de entorno
|
|
161
|
+
|
|
162
|
+
La librería no incluye un validador de env vars: cada proyecto tiene sus propios nombres y convenciones. Aquí un ejemplo con [zod](https://zod.dev) que puedes copiar a tu proyecto:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// src/env.ts
|
|
166
|
+
import { z } from 'zod';
|
|
167
|
+
|
|
168
|
+
const schema = z.object({
|
|
169
|
+
S3_BUCKET: z.string().min(1),
|
|
170
|
+
S3_REGION: z.string().default('auto'),
|
|
171
|
+
S3_ENDPOINT: z.string().url().optional(),
|
|
172
|
+
S3_ACCESS_KEY_ID: z.string().min(1),
|
|
173
|
+
S3_SECRET_ACCESS_KEY: z.string().min(1),
|
|
174
|
+
CDN_PUBLIC_URL: z.string().url(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const parsed = schema.safeParse(process.env);
|
|
178
|
+
if (!parsed.success) {
|
|
179
|
+
console.error('Invalid env', parsed.error.flatten().fieldErrors);
|
|
180
|
+
throw new Error('Invalid environment variables');
|
|
181
|
+
}
|
|
182
|
+
export const env = parsed.data;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Y úsalo:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { env } from './env';
|
|
189
|
+
import { S3StorageService } from '@codercito/object-storage';
|
|
190
|
+
|
|
191
|
+
export const storage = new S3StorageService({
|
|
192
|
+
bucket: env.S3_BUCKET,
|
|
193
|
+
region: env.S3_REGION,
|
|
194
|
+
endpoint: env.S3_ENDPOINT,
|
|
195
|
+
accessKeyId: env.S3_ACCESS_KEY_ID,
|
|
196
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## API
|
|
201
|
+
|
|
202
|
+
### `S3StorageService`
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
new S3StorageService(config: S3StorageConfig)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Campo | Tipo | Requerido | Descripción |
|
|
209
|
+
|-------|------|-----------|-------------|
|
|
210
|
+
| `bucket` | `string` | ✅ | Nombre del bucket. |
|
|
211
|
+
| `region` | `string` | ✅ | Región AWS o `'auto'` para R2. |
|
|
212
|
+
| `accessKeyId` | `string` | ✅ | Credencial. |
|
|
213
|
+
| `secretAccessKey` | `string` | ✅ | Credencial. |
|
|
214
|
+
| `endpoint` | `string` | — | Endpoint custom (R2, MinIO, etc.). |
|
|
215
|
+
| `forcePathStyle` | `boolean` | — | Necesario para MinIO. |
|
|
216
|
+
| `onError` | `(event) => void` | — | Callback de errores internos. |
|
|
217
|
+
|
|
218
|
+
#### `put(key, body, options): Promise<void>`
|
|
219
|
+
|
|
220
|
+
Sube un objeto. La key se sanea automáticamente.
|
|
221
|
+
|
|
222
|
+
`options` extiende `PutObjectCommandInput` del AWS SDK; los campos `contentType` y `cacheControl` se traducen a sus equivalentes (`ContentType`, `CacheControl`).
|
|
223
|
+
|
|
224
|
+
#### `exists(key): Promise<boolean>`
|
|
225
|
+
|
|
226
|
+
Devuelve `true` si el objeto existe. `false` en cualquier otro caso (incluye errores de red).
|
|
227
|
+
|
|
228
|
+
#### `presignedPost(request): Promise<PresignedPostResult>`
|
|
229
|
+
|
|
230
|
+
Genera credenciales para subida directa desde el cliente.
|
|
231
|
+
|
|
232
|
+
| Campo | Default | Descripción |
|
|
233
|
+
|-------|---------|-------------|
|
|
234
|
+
| `key` | — | Path destino. |
|
|
235
|
+
| `contentType` | — | MIME esperado, se valida en el POST. |
|
|
236
|
+
| `maxSizeBytes` | `10485760` (10 MB) | Tamaño máximo permitido. |
|
|
237
|
+
| `expiresIn` | `600` (10 min) | Validez en segundos. |
|
|
238
|
+
|
|
239
|
+
### `CdnFileUrlService`
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
new CdnFileUrlService(baseUrl: string)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Construye URLs públicas concatenando `baseUrl` con la key. **No re-sanitiza la key** — debes pasar la misma con la que subiste el objeto.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const urls = new CdnFileUrlService('https://cdn.miapp.com');
|
|
249
|
+
urls.getPublicUrl('avatars/user.jpg');
|
|
250
|
+
// → 'https://cdn.miapp.com/avatars/user.jpg'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `sanitizeKey(key): string`
|
|
254
|
+
|
|
255
|
+
Normaliza una key:
|
|
256
|
+
- Quita caracteres no permitidos (deja `[a-zA-Z0-9/_\-.]`).
|
|
257
|
+
- Colapsa slashes duplicados.
|
|
258
|
+
- Quita slashes al inicio y final.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
sanitizeKey('/avatars//user 123!.jpg');
|
|
262
|
+
// → 'avatars/user123.jpg'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Tipos
|
|
266
|
+
|
|
267
|
+
Todos los tipos están exportados:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import type {
|
|
271
|
+
IStorageService,
|
|
272
|
+
IFileUrlService,
|
|
273
|
+
S3StorageConfig,
|
|
274
|
+
PutObjectOptions,
|
|
275
|
+
PresignedPostRequest,
|
|
276
|
+
PresignedPostResult,
|
|
277
|
+
StorageErrorEvent,
|
|
278
|
+
} from '@codercito/object-storage';
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`IStorageService` es la interfaz principal; útil para inyectar implementaciones distintas (mocks en tests, etc.).
|
|
282
|
+
|
|
283
|
+
## Licencia
|
|
284
|
+
|
|
285
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { PutObjectCommandInput } from '@aws-sdk/client-s3';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opciones para subir un objeto. Extiende `PutObjectCommandInput` del AWS SDK
|
|
5
|
+
* para permitir cualquier parámetro adicional.
|
|
6
|
+
*/
|
|
7
|
+
interface PutObjectOptions extends Partial<PutObjectCommandInput> {
|
|
8
|
+
contentType: string;
|
|
9
|
+
cacheControl?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parámetros para generar un presigned POST.
|
|
13
|
+
*/
|
|
14
|
+
interface PresignedPostRequest {
|
|
15
|
+
/** Key (path) donde se subirá el archivo. */
|
|
16
|
+
key: string;
|
|
17
|
+
/** MIME type esperado. Se valida en la condición del POST. */
|
|
18
|
+
contentType: string;
|
|
19
|
+
/**
|
|
20
|
+
* Tamaño máximo permitido en bytes.
|
|
21
|
+
* @default 10485760 (10 MB)
|
|
22
|
+
*/
|
|
23
|
+
maxSizeBytes?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Tiempo de validez del presigned POST, en segundos.
|
|
26
|
+
* @default 600 (10 minutos)
|
|
27
|
+
*/
|
|
28
|
+
expiresIn?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resultado de un presigned POST. Se envía al cliente para que suba el archivo
|
|
32
|
+
* directamente al storage sin pasar por el backend.
|
|
33
|
+
*/
|
|
34
|
+
interface PresignedPostResult {
|
|
35
|
+
url: string;
|
|
36
|
+
fields: Record<string, string>;
|
|
37
|
+
key: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Evento que recibe el callback `onError` cuando algo falla internamente
|
|
41
|
+
* en una operación de storage. La librería no escribe a `console` por su cuenta;
|
|
42
|
+
* el consumidor decide cómo loggear.
|
|
43
|
+
*/
|
|
44
|
+
interface StorageErrorEvent {
|
|
45
|
+
/** Identificador corto del tipo de operación que falló. */
|
|
46
|
+
operation: 'upload_failed' | 'presign_failed';
|
|
47
|
+
/** Key del objeto involucrado. */
|
|
48
|
+
key: string;
|
|
49
|
+
/** Causa original lanzada por el SDK o la red. */
|
|
50
|
+
cause: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Configuración para `S3StorageService`. Compatible con S3, R2, MinIO,
|
|
54
|
+
* DigitalOcean Spaces y cualquier API S3-compatible.
|
|
55
|
+
*/
|
|
56
|
+
interface S3StorageConfig {
|
|
57
|
+
bucket: string;
|
|
58
|
+
region: string;
|
|
59
|
+
accessKeyId: string;
|
|
60
|
+
secretAccessKey: string;
|
|
61
|
+
/**
|
|
62
|
+
* Endpoint custom (solo necesario para providers no-AWS como R2 o MinIO).
|
|
63
|
+
*/
|
|
64
|
+
endpoint?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Si `true`, fuerza path-style URLs (`endpoint/bucket/key` en vez de
|
|
67
|
+
* `bucket.endpoint/key`). Necesario para MinIO y algunos proxies.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
forcePathStyle?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Callback opcional que se invoca cuando ocurre un error interno.
|
|
73
|
+
* Permite al consumidor loggear o reportar a su sistema preferido
|
|
74
|
+
* (Pino, Sentry, Datadog, etc.) sin que la librería decida por él.
|
|
75
|
+
*/
|
|
76
|
+
onError?: (event: StorageErrorEvent) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Interface principal del servicio de storage. Permite agregar otros providers
|
|
80
|
+
* en el futuro (R2 nativo, GCS, filesystem local) sin romper a los consumidores.
|
|
81
|
+
*/
|
|
82
|
+
interface IStorageService {
|
|
83
|
+
/** Sube un objeto al storage. */
|
|
84
|
+
put(key: string, body: string | Buffer, options: PutObjectOptions): Promise<void>;
|
|
85
|
+
/** Devuelve `true` si el objeto existe en el storage. */
|
|
86
|
+
exists(key: string): Promise<boolean>;
|
|
87
|
+
/** Genera un presigned POST para subida directa desde el cliente. */
|
|
88
|
+
presignedPost(request: PresignedPostRequest): Promise<PresignedPostResult>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Implementación de `IStorageService` para Amazon S3 y APIs S3-compatible
|
|
93
|
+
* (Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2).
|
|
94
|
+
*/
|
|
95
|
+
declare class S3StorageService implements IStorageService {
|
|
96
|
+
private readonly client;
|
|
97
|
+
private readonly bucket;
|
|
98
|
+
private readonly onError?;
|
|
99
|
+
constructor(config: S3StorageConfig);
|
|
100
|
+
put(key: string, body: string | Buffer, options: PutObjectOptions): Promise<void>;
|
|
101
|
+
exists(key: string): Promise<boolean>;
|
|
102
|
+
presignedPost({ key, contentType, maxSizeBytes, expiresIn, }: PresignedPostRequest): Promise<PresignedPostResult>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Servicio para construir URLs públicas a archivos almacenados.
|
|
107
|
+
* Útil cuando expones los objetos vía un CDN o el endpoint público del bucket.
|
|
108
|
+
*/
|
|
109
|
+
interface IFileUrlService {
|
|
110
|
+
getPublicUrl(key: string): string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Implementación basada en una base URL fija (típicamente un CDN como
|
|
114
|
+
* Cloudflare, CloudFront, o el endpoint público de R2/S3).
|
|
115
|
+
*
|
|
116
|
+
* No re-saniza la key: asume que se pasa la misma key con la que se subió
|
|
117
|
+
* el objeto. Sanitizar dos veces puede generar URLs que no apuntan al objeto
|
|
118
|
+
* real si la sanitización modifica la key.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const urls = new CdnFileUrlService('https://cdn.miapp.com');
|
|
123
|
+
* urls.getPublicUrl('avatars/user-123.jpg');
|
|
124
|
+
* // → 'https://cdn.miapp.com/avatars/user-123.jpg'
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare class CdnFileUrlService implements IFileUrlService {
|
|
128
|
+
private readonly baseUrl;
|
|
129
|
+
constructor(baseUrl: string);
|
|
130
|
+
getPublicUrl(key: string): string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normaliza una key para object storage:
|
|
135
|
+
* - Quita caracteres no permitidos (deja alfanuméricos, `/`, `_`, `-`, `.`).
|
|
136
|
+
* - Colapsa slashes duplicados (`a//b` → `a/b`).
|
|
137
|
+
* - Quita slashes al inicio y al final.
|
|
138
|
+
*
|
|
139
|
+
* Útil para sanear input del usuario antes de subir archivos.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* sanitizeKey('/avatars//user 123!.jpg') // 'avatars/user123.jpg'
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function sanitizeKey(key: string): string;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Error lanzado cuando una subida a storage falla.
|
|
150
|
+
* La causa original está disponible en `error.cause`.
|
|
151
|
+
*/
|
|
152
|
+
declare class StorageUploadError extends Error {
|
|
153
|
+
readonly key: string;
|
|
154
|
+
constructor(key: string, cause: unknown);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Error lanzado cuando la generación de un presigned POST falla.
|
|
158
|
+
*/
|
|
159
|
+
declare class StoragePresignError extends Error {
|
|
160
|
+
readonly key: string;
|
|
161
|
+
constructor(key: string, cause: unknown);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Error lanzado al construir un `S3StorageService` con configuración inválida.
|
|
165
|
+
*/
|
|
166
|
+
declare class StorageConfigError extends Error {
|
|
167
|
+
constructor(message: string);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { CdnFileUrlService, type IFileUrlService, type IStorageService, type PresignedPostRequest, type PresignedPostResult, type PutObjectOptions, type S3StorageConfig, S3StorageService, StorageConfigError, type StorageErrorEvent, StoragePresignError, StorageUploadError, sanitizeKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
3
|
+
|
|
4
|
+
// src/s3-storage.ts
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var StorageUploadError = class extends Error {
|
|
8
|
+
constructor(key, cause) {
|
|
9
|
+
super(`Failed to upload object: ${key}`, { cause });
|
|
10
|
+
this.key = key;
|
|
11
|
+
this.name = "StorageUploadError";
|
|
12
|
+
}
|
|
13
|
+
key;
|
|
14
|
+
};
|
|
15
|
+
var StoragePresignError = class extends Error {
|
|
16
|
+
constructor(key, cause) {
|
|
17
|
+
super(`Failed to generate presigned POST for: ${key}`, { cause });
|
|
18
|
+
this.key = key;
|
|
19
|
+
this.name = "StoragePresignError";
|
|
20
|
+
}
|
|
21
|
+
key;
|
|
22
|
+
};
|
|
23
|
+
var StorageConfigError = class extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "StorageConfigError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/utils.ts
|
|
31
|
+
function sanitizeKey(key) {
|
|
32
|
+
return key.replace(/[^a-zA-Z0-9/_\-.]/g, "").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/s3-storage.ts
|
|
36
|
+
var DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
37
|
+
var DEFAULT_EXPIRES_IN_SECONDS = 600;
|
|
38
|
+
var S3StorageService = class {
|
|
39
|
+
client;
|
|
40
|
+
bucket;
|
|
41
|
+
onError;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
if (!config.bucket) throw new StorageConfigError("Missing bucket");
|
|
44
|
+
if (!config.region) throw new StorageConfigError("Missing region");
|
|
45
|
+
if (!config.accessKeyId) throw new StorageConfigError("Missing accessKeyId");
|
|
46
|
+
if (!config.secretAccessKey) throw new StorageConfigError("Missing secretAccessKey");
|
|
47
|
+
this.bucket = config.bucket;
|
|
48
|
+
this.onError = config.onError;
|
|
49
|
+
this.client = new S3Client({
|
|
50
|
+
region: config.region,
|
|
51
|
+
endpoint: config.endpoint,
|
|
52
|
+
forcePathStyle: config.forcePathStyle,
|
|
53
|
+
credentials: {
|
|
54
|
+
accessKeyId: config.accessKeyId,
|
|
55
|
+
secretAccessKey: config.secretAccessKey
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async put(key, body, options) {
|
|
60
|
+
const safeKey = sanitizeKey(key);
|
|
61
|
+
try {
|
|
62
|
+
const { contentType, cacheControl, ...rest } = options;
|
|
63
|
+
await this.client.send(
|
|
64
|
+
new PutObjectCommand({
|
|
65
|
+
...rest,
|
|
66
|
+
Bucket: this.bucket,
|
|
67
|
+
Key: safeKey,
|
|
68
|
+
Body: body,
|
|
69
|
+
ContentType: contentType,
|
|
70
|
+
CacheControl: cacheControl
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
this.onError?.({ operation: "upload_failed", key: safeKey, cause });
|
|
75
|
+
throw new StorageUploadError(safeKey, cause);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async exists(key) {
|
|
79
|
+
const safeKey = sanitizeKey(key);
|
|
80
|
+
try {
|
|
81
|
+
await this.client.send(
|
|
82
|
+
new HeadObjectCommand({ Bucket: this.bucket, Key: safeKey })
|
|
83
|
+
);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async presignedPost({
|
|
90
|
+
key,
|
|
91
|
+
contentType,
|
|
92
|
+
maxSizeBytes = DEFAULT_MAX_SIZE_BYTES,
|
|
93
|
+
expiresIn = DEFAULT_EXPIRES_IN_SECONDS
|
|
94
|
+
}) {
|
|
95
|
+
const safeKey = sanitizeKey(key);
|
|
96
|
+
try {
|
|
97
|
+
const { url, fields } = await createPresignedPost(this.client, {
|
|
98
|
+
Bucket: this.bucket,
|
|
99
|
+
Key: safeKey,
|
|
100
|
+
Conditions: [
|
|
101
|
+
["content-length-range", 0, maxSizeBytes],
|
|
102
|
+
["eq", "$Content-Type", contentType]
|
|
103
|
+
],
|
|
104
|
+
Fields: {
|
|
105
|
+
"Content-Type": contentType
|
|
106
|
+
},
|
|
107
|
+
Expires: expiresIn
|
|
108
|
+
});
|
|
109
|
+
return { url, fields, key: safeKey };
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
this.onError?.({ operation: "presign_failed", key: safeKey, cause });
|
|
112
|
+
throw new StoragePresignError(safeKey, cause);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/file-url.ts
|
|
118
|
+
var CdnFileUrlService = class {
|
|
119
|
+
baseUrl;
|
|
120
|
+
constructor(baseUrl) {
|
|
121
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
122
|
+
}
|
|
123
|
+
getPublicUrl(key) {
|
|
124
|
+
const cleanKey = key.replace(/^\/+/, "");
|
|
125
|
+
return `${this.baseUrl}/${cleanKey}`;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export { CdnFileUrlService, S3StorageService, StorageConfigError, StoragePresignError, StorageUploadError, sanitizeKey };
|
|
130
|
+
//# sourceMappingURL=index.js.map
|
|
131
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/utils.ts","../src/s3-storage.ts","../src/file-url.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error lanzado cuando una subida a storage falla.\n * La causa original está disponible en `error.cause`.\n */\nexport class StorageUploadError extends Error {\n constructor(public readonly key: string, cause: unknown) {\n super(`Failed to upload object: ${key}`, { cause });\n this.name = 'StorageUploadError';\n }\n}\n\n/**\n * Error lanzado cuando la generación de un presigned POST falla.\n */\nexport class StoragePresignError extends Error {\n constructor(public readonly key: string, cause: unknown) {\n super(`Failed to generate presigned POST for: ${key}`, { cause });\n this.name = 'StoragePresignError';\n }\n}\n\n/**\n * Error lanzado al construir un `S3StorageService` con configuración inválida.\n */\nexport class StorageConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StorageConfigError';\n }\n}\n","/**\n * Normaliza una key para object storage:\n * - Quita caracteres no permitidos (deja alfanuméricos, `/`, `_`, `-`, `.`).\n * - Colapsa slashes duplicados (`a//b` → `a/b`).\n * - Quita slashes al inicio y al final.\n *\n * Útil para sanear input del usuario antes de subir archivos.\n *\n * @example\n * ```ts\n * sanitizeKey('/avatars//user 123!.jpg') // 'avatars/user123.jpg'\n * ```\n */\nexport function sanitizeKey(key: string): string {\n return key\n .replace(/[^a-zA-Z0-9/_\\-.]/g, '')\n .replace(/\\/+/g, '/')\n .replace(/^\\/|\\/$/g, '');\n}\n","import {\n S3Client,\n PutObjectCommand,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3';\nimport { createPresignedPost } from '@aws-sdk/s3-presigned-post';\n\nimport type {\n IStorageService,\n PutObjectOptions,\n PresignedPostRequest,\n PresignedPostResult,\n S3StorageConfig,\n StorageErrorEvent,\n} from './types';\n\nimport {\n StorageUploadError,\n StoragePresignError,\n StorageConfigError,\n} from './errors';\n\nimport { sanitizeKey } from './utils';\n\nconst DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;\nconst DEFAULT_EXPIRES_IN_SECONDS = 600;\n\n/**\n * Implementación de `IStorageService` para Amazon S3 y APIs S3-compatible\n * (Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2).\n */\nexport class S3StorageService implements IStorageService {\n private readonly client: S3Client;\n private readonly bucket: string;\n private readonly onError?: (event: StorageErrorEvent) => void;\n\n constructor(config: S3StorageConfig) {\n if (!config.bucket) throw new StorageConfigError('Missing bucket');\n if (!config.region) throw new StorageConfigError('Missing region');\n if (!config.accessKeyId) throw new StorageConfigError('Missing accessKeyId');\n if (!config.secretAccessKey) throw new StorageConfigError('Missing secretAccessKey');\n\n this.bucket = config.bucket;\n this.onError = config.onError;\n\n this.client = new S3Client({\n region: config.region,\n endpoint: config.endpoint,\n forcePathStyle: config.forcePathStyle,\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n });\n }\n\n async put(\n key: string,\n body: string | Buffer,\n options: PutObjectOptions\n ): Promise<void> {\n const safeKey = sanitizeKey(key);\n\n try {\n const { contentType, cacheControl, ...rest } = options;\n\n await this.client.send(\n new PutObjectCommand({\n ...rest,\n Bucket: this.bucket,\n Key: safeKey,\n Body: body,\n ContentType: contentType,\n CacheControl: cacheControl,\n })\n );\n } catch (cause) {\n this.onError?.({ operation: 'upload_failed', key: safeKey, cause });\n throw new StorageUploadError(safeKey, cause);\n }\n }\n\n async exists(key: string): Promise<boolean> {\n const safeKey = sanitizeKey(key);\n try {\n await this.client.send(\n new HeadObjectCommand({ Bucket: this.bucket, Key: safeKey })\n );\n return true;\n } catch {\n // No distinguimos entre NotFound y otros errores: si algo falla,\n // tratamos el objeto como inexistente. Si el consumidor necesita\n // distinguir, puede observar `onError`.\n return false;\n }\n }\n\n async presignedPost({\n key,\n contentType,\n maxSizeBytes = DEFAULT_MAX_SIZE_BYTES,\n expiresIn = DEFAULT_EXPIRES_IN_SECONDS,\n }: PresignedPostRequest): Promise<PresignedPostResult> {\n const safeKey = sanitizeKey(key);\n\n try {\n const { url, fields } = await createPresignedPost(this.client, {\n Bucket: this.bucket,\n Key: safeKey,\n Conditions: [\n ['content-length-range', 0, maxSizeBytes],\n ['eq', '$Content-Type', contentType],\n ],\n Fields: {\n 'Content-Type': contentType,\n },\n Expires: expiresIn,\n });\n\n return { url, fields, key: safeKey };\n } catch (cause) {\n this.onError?.({ operation: 'presign_failed', key: safeKey, cause });\n throw new StoragePresignError(safeKey, cause);\n }\n }\n}\n","/**\n * Servicio para construir URLs públicas a archivos almacenados.\n * Útil cuando expones los objetos vía un CDN o el endpoint público del bucket.\n */\nexport interface IFileUrlService {\n getPublicUrl(key: string): string;\n}\n\n/**\n * Implementación basada en una base URL fija (típicamente un CDN como\n * Cloudflare, CloudFront, o el endpoint público de R2/S3).\n *\n * No re-saniza la key: asume que se pasa la misma key con la que se subió\n * el objeto. Sanitizar dos veces puede generar URLs que no apuntan al objeto\n * real si la sanitización modifica la key.\n *\n * @example\n * ```ts\n * const urls = new CdnFileUrlService('https://cdn.miapp.com');\n * urls.getPublicUrl('avatars/user-123.jpg');\n * // → 'https://cdn.miapp.com/avatars/user-123.jpg'\n * ```\n */\nexport class CdnFileUrlService implements IFileUrlService {\n private readonly baseUrl: string;\n\n constructor(baseUrl: string) {\n // Quitar trailing slash para evitar dobles slashes al concatenar\n this.baseUrl = baseUrl.replace(/\\/+$/, '');\n }\n\n getPublicUrl(key: string): string {\n // Quitar leading slash de la key para evitar dobles slashes\n const cleanKey = key.replace(/^\\/+/, '');\n return `${this.baseUrl}/${cleanKey}`;\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codercito/object-storage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Abstracción tipada para object storage (S3-compatible) con CDN URL helpers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"type-check": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
26
|
+
"@aws-sdk/s3-presigned-post": "^3.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@aws-sdk/client-s3": "^3.616.0",
|
|
30
|
+
"@aws-sdk/s3-presigned-post": "^3.616.0",
|
|
31
|
+
"@types/node": "^20.11.0",
|
|
32
|
+
"tsup": "^8.0.2",
|
|
33
|
+
"typescript": "^5.3.3"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"s3",
|
|
37
|
+
"storage",
|
|
38
|
+
"object-storage",
|
|
39
|
+
"aws",
|
|
40
|
+
"r2",
|
|
41
|
+
"cloudflare",
|
|
42
|
+
"minio",
|
|
43
|
+
"presigned"
|
|
44
|
+
],
|
|
45
|
+
"author": "",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": ""
|
|
53
|
+
}
|
|
54
|
+
}
|