@vlsdev/s3-portable 1.0.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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @vs/s3-portable
2
+
3
+ Pacote TypeScript "portable" para padronizar integração com Amazon S3 em microserviços, com arquitetura em camadas (DDD + SOLID).
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm i @vs/s3-portable
9
+ ```
10
+
11
+ ## Variáveis de ambiente esperadas
12
+
13
+ ```env
14
+ AWS_REGION=us-east-1
15
+ AWS_ACCESS_KEY_ID=...
16
+ AWS_SECRET_ACCESS_KEY=...
17
+ AWS_S3_BUCKET_NAME=...
18
+ ```
19
+
20
+ ## Uso rápido (via env)
21
+
22
+ ```ts
23
+ import "dotenv/config";
24
+ import { createS3ServiceFromEnv } from "@vs/s3-portable";
25
+
26
+ const s3Service = createS3ServiceFromEnv();
27
+
28
+ const body = await s3Service.downloadFile("meu-projeto", "documentos", "arquivo.pdf");
29
+ const files = await s3Service.listFiles("meu-projeto", "documentos");
30
+ await s3Service.deleteFile("meu-projeto", "documentos", "arquivo.pdf");
31
+ const url = await s3Service.generateSignedUrl("meu-projeto", "documentos", "arquivo.pdf", 3600);
32
+ ```
33
+
34
+ ## Uso com configuração explícita
35
+
36
+ ```ts
37
+ import { createS3Service } from "@vs/s3-portable";
38
+
39
+ const s3Service = createS3Service({
40
+ region: "us-east-1",
41
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
42
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
43
+ bucketName: process.env.AWS_S3_BUCKET_NAME!,
44
+ });
45
+ ```
46
+
47
+ ## API
48
+
49
+ - `downloadFile(project, path, fileName)`
50
+ - `listFiles(project, path)`
51
+ - `deleteFile(project, path, fileName)`
52
+ - `generateSignedUrl(project, path, fileName, expiresIn?)`
53
+
54
+ ## Build
55
+
56
+ ```bash
57
+ npm run build
58
+ ```
59
+
60
+ ## Publicacao privada no npm
61
+
62
+ 1. Login na conta correta:
63
+
64
+ ```bash
65
+ npm whoami
66
+ ```
67
+
68
+ 2. Publicar com 2FA (OTP interativo):
69
+
70
+ ```bash
71
+ npm run publish:private:otp
72
+ ```
73
+
74
+ 3. Ou publicar sem OTP usando token granular com permissao de publish + bypass 2FA:
75
+
76
+ ```bash
77
+ npm run publish:private
78
+ ```
79
+
80
+ ### Troubleshooting E403 na publicacao
81
+
82
+ Se aparecer:
83
+
84
+ `403 Forbidden ... Two-factor authentication or granular access token with bypass 2fa enabled is required to publish packages.`
85
+
86
+ valide estes pontos:
87
+
88
+ - O usuario autenticado tem permissao de publish no escopo `@vs` (owner/maintainer).
89
+ - Se usar conta com 2FA obrigatorio para escrita, publique com OTP (`npm run publish:private:otp`).
90
+ - Se usar token, ele precisa ser granular com permissao de publish para `@vs/*` e com bypass 2FA habilitado.
91
+ - Remova credenciais antigas e relogue:
92
+
93
+ ```bash
94
+ npm logout
95
+ npm login
96
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DomainError: () => DomainError,
24
+ S3Service: () => S3Service,
25
+ createS3Service: () => createS3Service,
26
+ createS3ServiceFromEnv: () => createS3ServiceFromEnv
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/domain/errors/DomainError.ts
31
+ var DomainError = class extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "DomainError";
35
+ }
36
+ };
37
+
38
+ // src/domain/value-objects/S3ObjectPath.ts
39
+ var normalizeSegment = (value, label) => {
40
+ const sanitized = value.trim().replace(/^\/+|\/+$/g, "");
41
+ if (!sanitized) {
42
+ throw new DomainError(`${label} is required`);
43
+ }
44
+ if (sanitized.includes("..")) {
45
+ throw new DomainError(`${label} cannot contain ".."`);
46
+ }
47
+ return sanitized;
48
+ };
49
+ var S3ObjectPath = class {
50
+ static build(project, path, fileName) {
51
+ const projectPart = normalizeSegment(project, "project");
52
+ const pathPart = normalizeSegment(path, "path");
53
+ if (!fileName) {
54
+ return `${projectPart}/${pathPart}/`;
55
+ }
56
+ const filePart = normalizeSegment(fileName, "fileName");
57
+ return `${projectPart}/${pathPart}/${filePart}`;
58
+ }
59
+ };
60
+
61
+ // src/application/use-cases/DeleteFileUseCase.ts
62
+ var DeleteFileUseCase = class {
63
+ constructor(gateway) {
64
+ this.gateway = gateway;
65
+ }
66
+ /**
67
+ * Executa a remoção de um arquivo.
68
+ *
69
+ * @param input Dados de identificação do arquivo.
70
+ * @returns Mensagem de confirmação de exclusão.
71
+ */
72
+ async execute(input) {
73
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
74
+ await this.gateway.deleteObject(objectKey);
75
+ return { message: "Arquivo deletado com sucesso" };
76
+ }
77
+ };
78
+
79
+ // src/application/use-cases/DownloadFileUseCase.ts
80
+ var DownloadFileUseCase = class {
81
+ constructor(gateway) {
82
+ this.gateway = gateway;
83
+ }
84
+ /**
85
+ * Executa o download de um arquivo.
86
+ *
87
+ * @param input Dados de identificação do arquivo.
88
+ * @returns Corpo do objeto retornado pelo SDK AWS (stream/blob).
89
+ */
90
+ async execute(input) {
91
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
92
+ return this.gateway.getObject(objectKey);
93
+ }
94
+ };
95
+
96
+ // src/application/use-cases/GenerateSignedUrlUseCase.ts
97
+ var GenerateSignedUrlUseCase = class {
98
+ constructor(gateway) {
99
+ this.gateway = gateway;
100
+ }
101
+ /**
102
+ * Executa a geração da URL assinada.
103
+ *
104
+ * Regra de negócio: `expiresIn` deve ser maior que zero.
105
+ *
106
+ * @param input Dados de identificação do arquivo e expiração opcional.
107
+ * @returns URL assinada para download temporário.
108
+ */
109
+ async execute(input) {
110
+ const expiresIn = input.expiresIn ?? 3600;
111
+ if (expiresIn <= 0) {
112
+ throw new DomainError("expiresIn must be greater than zero");
113
+ }
114
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
115
+ return this.gateway.generateGetSignedUrl(objectKey, expiresIn);
116
+ }
117
+ };
118
+
119
+ // src/application/use-cases/ListFilesUseCase.ts
120
+ var ListFilesUseCase = class {
121
+ constructor(gateway) {
122
+ this.gateway = gateway;
123
+ }
124
+ /**
125
+ * Executa a listagem de chaves no S3.
126
+ *
127
+ * @param input Dados de projeto e caminho.
128
+ * @returns Lista de chaves completas encontradas no bucket.
129
+ */
130
+ async execute(input) {
131
+ const prefix = S3ObjectPath.build(input.project, input.path);
132
+ return this.gateway.listObjects(prefix);
133
+ }
134
+ };
135
+
136
+ // src/facade/S3Service.ts
137
+ var S3Service = class {
138
+ constructor(downloadFileUseCase, listFilesUseCase, deleteFileUseCase, generateSignedUrlUseCase) {
139
+ this.downloadFileUseCase = downloadFileUseCase;
140
+ this.listFilesUseCase = listFilesUseCase;
141
+ this.deleteFileUseCase = deleteFileUseCase;
142
+ this.generateSignedUrlUseCase = generateSignedUrlUseCase;
143
+ }
144
+ async downloadFile(project, path, fileName) {
145
+ return this.downloadFileUseCase.execute({ project, path, fileName });
146
+ }
147
+ async listFiles(project, path) {
148
+ return this.listFilesUseCase.execute({ project, path });
149
+ }
150
+ async deleteFile(project, path, fileName) {
151
+ return this.deleteFileUseCase.execute({ project, path, fileName });
152
+ }
153
+ async generateSignedUrl(project, path, fileName, expiresIn = 10800) {
154
+ return this.generateSignedUrlUseCase.execute({
155
+ project,
156
+ path,
157
+ fileName,
158
+ expiresIn
159
+ });
160
+ }
161
+ };
162
+
163
+ // src/infrastructure/aws/AwsS3StorageGateway.ts
164
+ var import_client_s3 = require("@aws-sdk/client-s3");
165
+ var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
166
+ var AwsS3StorageGateway = class {
167
+ constructor(config, client) {
168
+ this.config = config;
169
+ this.client = client ?? new import_client_s3.S3Client({
170
+ region: config.region,
171
+ credentials: {
172
+ accessKeyId: config.accessKeyId,
173
+ secretAccessKey: config.secretAccessKey
174
+ }
175
+ });
176
+ }
177
+ client;
178
+ async getObject(objectKey) {
179
+ const { Body } = await this.client.send(
180
+ new import_client_s3.GetObjectCommand({
181
+ Bucket: this.config.bucketName,
182
+ Key: objectKey
183
+ })
184
+ );
185
+ if (!Body) {
186
+ throw new Error(`Object "${objectKey}" returned empty body`);
187
+ }
188
+ return Body;
189
+ }
190
+ async listObjects(prefix) {
191
+ const { Contents } = await this.client.send(
192
+ new import_client_s3.ListObjectsV2Command({
193
+ Bucket: this.config.bucketName,
194
+ Prefix: prefix
195
+ })
196
+ );
197
+ return Contents?.map((file) => file.Key).filter((key) => Boolean(key)) ?? [];
198
+ }
199
+ async deleteObject(objectKey) {
200
+ await this.client.send(
201
+ new import_client_s3.DeleteObjectCommand({
202
+ Bucket: this.config.bucketName,
203
+ Key: objectKey
204
+ })
205
+ );
206
+ }
207
+ async generateGetSignedUrl(objectKey, expiresInSeconds) {
208
+ const command = new import_client_s3.GetObjectCommand({
209
+ Bucket: this.config.bucketName,
210
+ Key: objectKey
211
+ });
212
+ return (0, import_s3_request_presigner.getSignedUrl)(this.client, command, { expiresIn: expiresInSeconds });
213
+ }
214
+ };
215
+
216
+ // src/infrastructure/config/S3Config.ts
217
+ var createS3Config = (input) => {
218
+ const config = {
219
+ region: input.region?.trim() ?? "",
220
+ accessKeyId: input.accessKeyId?.trim() ?? "",
221
+ secretAccessKey: input.secretAccessKey?.trim() ?? "",
222
+ bucketName: input.bucketName?.trim() ?? ""
223
+ };
224
+ if (!config.region) throw new DomainError("AWS region is required");
225
+ if (!config.accessKeyId) throw new DomainError("AWS access key id is required");
226
+ if (!config.secretAccessKey) throw new DomainError("AWS secret access key is required");
227
+ if (!config.bucketName) throw new DomainError("AWS S3 bucket name is required");
228
+ return config;
229
+ };
230
+ var createS3ConfigFromEnv = (env = process.env) => {
231
+ return createS3Config({
232
+ region: env.AWS_REGION,
233
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
234
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
235
+ bucketName: env.AWS_S3_BUCKET_NAME
236
+ });
237
+ };
238
+
239
+ // src/index.ts
240
+ var buildService = (config) => {
241
+ const gateway = new AwsS3StorageGateway(config);
242
+ const downloadFileUseCase = new DownloadFileUseCase(gateway);
243
+ const listFilesUseCase = new ListFilesUseCase(gateway);
244
+ const deleteFileUseCase = new DeleteFileUseCase(gateway);
245
+ const generateSignedUrlUseCase = new GenerateSignedUrlUseCase(gateway);
246
+ return new S3Service(
247
+ downloadFileUseCase,
248
+ listFilesUseCase,
249
+ deleteFileUseCase,
250
+ generateSignedUrlUseCase
251
+ );
252
+ };
253
+ var createS3Service = (config) => {
254
+ return buildService(createS3Config(config));
255
+ };
256
+ var createS3ServiceFromEnv = (env = process.env) => {
257
+ return buildService(createS3ConfigFromEnv(env));
258
+ };
259
+ // Annotate the CommonJS export names for ESM import in node:
260
+ 0 && (module.exports = {
261
+ DomainError,
262
+ S3Service,
263
+ createS3Service,
264
+ createS3ServiceFromEnv
265
+ });
@@ -0,0 +1,191 @@
1
+ import { GetObjectCommandOutput } from '@aws-sdk/client-s3';
2
+
3
+ /**
4
+ * Corpo retornado pelo S3 em operações de download.
5
+ * É não-nulo por contrato para simplificar os casos de uso.
6
+ */
7
+ type S3ObjectBody = NonNullable<GetObjectCommandOutput["Body"]>;
8
+ /**
9
+ * Porta de saída da aplicação para operações de armazenamento no S3.
10
+ * Casos de uso dependem desta interface, não da implementação AWS.
11
+ */
12
+ interface S3StorageGateway {
13
+ /**
14
+ * Obtém o objeto do S3 a partir da chave completa.
15
+ *
16
+ * @param objectKey Chave completa do objeto (ex.: `projeto/pasta/arquivo.pdf`).
17
+ * @returns Corpo do arquivo em stream/blob conforme SDK AWS.
18
+ */
19
+ getObject(objectKey: string): Promise<S3ObjectBody>;
20
+ /**
21
+ * Lista chaves de objetos a partir de um prefixo.
22
+ *
23
+ * Em geral o prefixo termina com `/` para simular "pasta".
24
+ * Ex.: `projeto/pasta/`.
25
+ *
26
+ * @param prefix Prefixo de busca no bucket.
27
+ * @returns Lista de chaves completas encontradas.
28
+ */
29
+ listObjects(prefix: string): Promise<string[]>;
30
+ /**
31
+ * Remove um objeto do bucket pela chave completa.
32
+ *
33
+ * @param objectKey Chave completa do objeto a ser removido.
34
+ */
35
+ deleteObject(objectKey: string): Promise<void>;
36
+ /**
37
+ * Gera URL assinada para download (`GET`) de um objeto.
38
+ *
39
+ * @param objectKey Chave completa do objeto.
40
+ * @param expiresInSeconds Tempo de expiração da URL, em segundos.
41
+ * @returns URL temporária assinada.
42
+ */
43
+ generateGetSignedUrl(objectKey: string, expiresInSeconds: number): Promise<string>;
44
+ }
45
+
46
+ /**
47
+ * Dados necessários para remover um arquivo do S3.
48
+ */
49
+ interface DeleteFileInput {
50
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
51
+ project: string;
52
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
53
+ path: string;
54
+ /** Nome final do arquivo a ser removido. */
55
+ fileName: string;
56
+ }
57
+ /**
58
+ * Caso de uso responsável por remover um objeto no S3.
59
+ */
60
+ declare class DeleteFileUseCase {
61
+ private readonly gateway;
62
+ constructor(gateway: S3StorageGateway);
63
+ /**
64
+ * Executa a remoção de um arquivo.
65
+ *
66
+ * @param input Dados de identificação do arquivo.
67
+ * @returns Mensagem de confirmação de exclusão.
68
+ */
69
+ execute(input: DeleteFileInput): Promise<{
70
+ message: string;
71
+ }>;
72
+ }
73
+
74
+ /**
75
+ * Dados necessários para baixar um arquivo do S3.
76
+ */
77
+ interface DownloadFileInput {
78
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
79
+ project: string;
80
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
81
+ path: string;
82
+ /** Nome final do arquivo a ser baixado. */
83
+ fileName: string;
84
+ }
85
+ /**
86
+ * Caso de uso responsável por baixar um objeto do S3.
87
+ * Constrói a chave no padrão de domínio e delega o acesso à porta de infraestrutura.
88
+ */
89
+ declare class DownloadFileUseCase {
90
+ private readonly gateway;
91
+ constructor(gateway: S3StorageGateway);
92
+ /**
93
+ * Executa o download de um arquivo.
94
+ *
95
+ * @param input Dados de identificação do arquivo.
96
+ * @returns Corpo do objeto retornado pelo SDK AWS (stream/blob).
97
+ */
98
+ execute(input: DownloadFileInput): Promise<S3ObjectBody>;
99
+ }
100
+
101
+ /**
102
+ * Dados necessários para gerar URL assinada de download.
103
+ */
104
+ interface GenerateSignedUrlInput {
105
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
106
+ project: string;
107
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
108
+ path: string;
109
+ /** Nome final do arquivo para gerar a URL assinada. */
110
+ fileName: string;
111
+ /** Expiração da URL em segundos. Padrão: 3600. */
112
+ expiresIn?: number;
113
+ }
114
+ /**
115
+ * Caso de uso responsável por gerar URL assinada (GET) para um objeto do S3.
116
+ */
117
+ declare class GenerateSignedUrlUseCase {
118
+ private readonly gateway;
119
+ constructor(gateway: S3StorageGateway);
120
+ /**
121
+ * Executa a geração da URL assinada.
122
+ *
123
+ * Regra de negócio: `expiresIn` deve ser maior que zero.
124
+ *
125
+ * @param input Dados de identificação do arquivo e expiração opcional.
126
+ * @returns URL assinada para download temporário.
127
+ */
128
+ execute(input: GenerateSignedUrlInput): Promise<string>;
129
+ }
130
+
131
+ /**
132
+ * Dados necessários para listar arquivos de um caminho lógico no S3.
133
+ */
134
+ interface ListFilesInput {
135
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
136
+ project: string;
137
+ /** Caminho lógico a ser listado dentro do projeto. */
138
+ path: string;
139
+ }
140
+ /**
141
+ * Caso de uso responsável por listar arquivos a partir de um prefixo.
142
+ */
143
+ declare class ListFilesUseCase {
144
+ private readonly gateway;
145
+ constructor(gateway: S3StorageGateway);
146
+ /**
147
+ * Executa a listagem de chaves no S3.
148
+ *
149
+ * @param input Dados de projeto e caminho.
150
+ * @returns Lista de chaves completas encontradas no bucket.
151
+ */
152
+ execute(input: ListFilesInput): Promise<string[]>;
153
+ }
154
+
155
+ interface IS3Service {
156
+ downloadFile(project: string, path: string, fileName: string): Promise<S3ObjectBody>;
157
+ listFiles(project: string, path: string): Promise<string[]>;
158
+ deleteFile(project: string, path: string, fileName: string): Promise<{
159
+ message: string;
160
+ }>;
161
+ generateSignedUrl(project: string, path: string, fileName: string, expiresIn?: number): Promise<string>;
162
+ }
163
+ declare class S3Service implements IS3Service {
164
+ private readonly downloadFileUseCase;
165
+ private readonly listFilesUseCase;
166
+ private readonly deleteFileUseCase;
167
+ private readonly generateSignedUrlUseCase;
168
+ constructor(downloadFileUseCase: DownloadFileUseCase, listFilesUseCase: ListFilesUseCase, deleteFileUseCase: DeleteFileUseCase, generateSignedUrlUseCase: GenerateSignedUrlUseCase);
169
+ downloadFile(project: string, path: string, fileName: string): Promise<S3ObjectBody>;
170
+ listFiles(project: string, path: string): Promise<string[]>;
171
+ deleteFile(project: string, path: string, fileName: string): Promise<{
172
+ message: string;
173
+ }>;
174
+ generateSignedUrl(project: string, path: string, fileName: string, expiresIn?: number): Promise<string>;
175
+ }
176
+
177
+ interface S3Config {
178
+ region: string;
179
+ accessKeyId: string;
180
+ secretAccessKey: string;
181
+ bucketName: string;
182
+ }
183
+
184
+ declare class DomainError extends Error {
185
+ constructor(message: string);
186
+ }
187
+
188
+ declare const createS3Service: (config: S3Config) => S3Service;
189
+ declare const createS3ServiceFromEnv: (env?: NodeJS.ProcessEnv) => S3Service;
190
+
191
+ export { DomainError, type IS3Service, type S3Config, S3Service, createS3Service, createS3ServiceFromEnv };
@@ -0,0 +1,191 @@
1
+ import { GetObjectCommandOutput } from '@aws-sdk/client-s3';
2
+
3
+ /**
4
+ * Corpo retornado pelo S3 em operações de download.
5
+ * É não-nulo por contrato para simplificar os casos de uso.
6
+ */
7
+ type S3ObjectBody = NonNullable<GetObjectCommandOutput["Body"]>;
8
+ /**
9
+ * Porta de saída da aplicação para operações de armazenamento no S3.
10
+ * Casos de uso dependem desta interface, não da implementação AWS.
11
+ */
12
+ interface S3StorageGateway {
13
+ /**
14
+ * Obtém o objeto do S3 a partir da chave completa.
15
+ *
16
+ * @param objectKey Chave completa do objeto (ex.: `projeto/pasta/arquivo.pdf`).
17
+ * @returns Corpo do arquivo em stream/blob conforme SDK AWS.
18
+ */
19
+ getObject(objectKey: string): Promise<S3ObjectBody>;
20
+ /**
21
+ * Lista chaves de objetos a partir de um prefixo.
22
+ *
23
+ * Em geral o prefixo termina com `/` para simular "pasta".
24
+ * Ex.: `projeto/pasta/`.
25
+ *
26
+ * @param prefix Prefixo de busca no bucket.
27
+ * @returns Lista de chaves completas encontradas.
28
+ */
29
+ listObjects(prefix: string): Promise<string[]>;
30
+ /**
31
+ * Remove um objeto do bucket pela chave completa.
32
+ *
33
+ * @param objectKey Chave completa do objeto a ser removido.
34
+ */
35
+ deleteObject(objectKey: string): Promise<void>;
36
+ /**
37
+ * Gera URL assinada para download (`GET`) de um objeto.
38
+ *
39
+ * @param objectKey Chave completa do objeto.
40
+ * @param expiresInSeconds Tempo de expiração da URL, em segundos.
41
+ * @returns URL temporária assinada.
42
+ */
43
+ generateGetSignedUrl(objectKey: string, expiresInSeconds: number): Promise<string>;
44
+ }
45
+
46
+ /**
47
+ * Dados necessários para remover um arquivo do S3.
48
+ */
49
+ interface DeleteFileInput {
50
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
51
+ project: string;
52
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
53
+ path: string;
54
+ /** Nome final do arquivo a ser removido. */
55
+ fileName: string;
56
+ }
57
+ /**
58
+ * Caso de uso responsável por remover um objeto no S3.
59
+ */
60
+ declare class DeleteFileUseCase {
61
+ private readonly gateway;
62
+ constructor(gateway: S3StorageGateway);
63
+ /**
64
+ * Executa a remoção de um arquivo.
65
+ *
66
+ * @param input Dados de identificação do arquivo.
67
+ * @returns Mensagem de confirmação de exclusão.
68
+ */
69
+ execute(input: DeleteFileInput): Promise<{
70
+ message: string;
71
+ }>;
72
+ }
73
+
74
+ /**
75
+ * Dados necessários para baixar um arquivo do S3.
76
+ */
77
+ interface DownloadFileInput {
78
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
79
+ project: string;
80
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
81
+ path: string;
82
+ /** Nome final do arquivo a ser baixado. */
83
+ fileName: string;
84
+ }
85
+ /**
86
+ * Caso de uso responsável por baixar um objeto do S3.
87
+ * Constrói a chave no padrão de domínio e delega o acesso à porta de infraestrutura.
88
+ */
89
+ declare class DownloadFileUseCase {
90
+ private readonly gateway;
91
+ constructor(gateway: S3StorageGateway);
92
+ /**
93
+ * Executa o download de um arquivo.
94
+ *
95
+ * @param input Dados de identificação do arquivo.
96
+ * @returns Corpo do objeto retornado pelo SDK AWS (stream/blob).
97
+ */
98
+ execute(input: DownloadFileInput): Promise<S3ObjectBody>;
99
+ }
100
+
101
+ /**
102
+ * Dados necessários para gerar URL assinada de download.
103
+ */
104
+ interface GenerateSignedUrlInput {
105
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
106
+ project: string;
107
+ /** Caminho lógico dentro do projeto (sem o nome do arquivo). */
108
+ path: string;
109
+ /** Nome final do arquivo para gerar a URL assinada. */
110
+ fileName: string;
111
+ /** Expiração da URL em segundos. Padrão: 3600. */
112
+ expiresIn?: number;
113
+ }
114
+ /**
115
+ * Caso de uso responsável por gerar URL assinada (GET) para um objeto do S3.
116
+ */
117
+ declare class GenerateSignedUrlUseCase {
118
+ private readonly gateway;
119
+ constructor(gateway: S3StorageGateway);
120
+ /**
121
+ * Executa a geração da URL assinada.
122
+ *
123
+ * Regra de negócio: `expiresIn` deve ser maior que zero.
124
+ *
125
+ * @param input Dados de identificação do arquivo e expiração opcional.
126
+ * @returns URL assinada para download temporário.
127
+ */
128
+ execute(input: GenerateSignedUrlInput): Promise<string>;
129
+ }
130
+
131
+ /**
132
+ * Dados necessários para listar arquivos de um caminho lógico no S3.
133
+ */
134
+ interface ListFilesInput {
135
+ /** Nome lógico do projeto (primeiro segmento da chave S3). */
136
+ project: string;
137
+ /** Caminho lógico a ser listado dentro do projeto. */
138
+ path: string;
139
+ }
140
+ /**
141
+ * Caso de uso responsável por listar arquivos a partir de um prefixo.
142
+ */
143
+ declare class ListFilesUseCase {
144
+ private readonly gateway;
145
+ constructor(gateway: S3StorageGateway);
146
+ /**
147
+ * Executa a listagem de chaves no S3.
148
+ *
149
+ * @param input Dados de projeto e caminho.
150
+ * @returns Lista de chaves completas encontradas no bucket.
151
+ */
152
+ execute(input: ListFilesInput): Promise<string[]>;
153
+ }
154
+
155
+ interface IS3Service {
156
+ downloadFile(project: string, path: string, fileName: string): Promise<S3ObjectBody>;
157
+ listFiles(project: string, path: string): Promise<string[]>;
158
+ deleteFile(project: string, path: string, fileName: string): Promise<{
159
+ message: string;
160
+ }>;
161
+ generateSignedUrl(project: string, path: string, fileName: string, expiresIn?: number): Promise<string>;
162
+ }
163
+ declare class S3Service implements IS3Service {
164
+ private readonly downloadFileUseCase;
165
+ private readonly listFilesUseCase;
166
+ private readonly deleteFileUseCase;
167
+ private readonly generateSignedUrlUseCase;
168
+ constructor(downloadFileUseCase: DownloadFileUseCase, listFilesUseCase: ListFilesUseCase, deleteFileUseCase: DeleteFileUseCase, generateSignedUrlUseCase: GenerateSignedUrlUseCase);
169
+ downloadFile(project: string, path: string, fileName: string): Promise<S3ObjectBody>;
170
+ listFiles(project: string, path: string): Promise<string[]>;
171
+ deleteFile(project: string, path: string, fileName: string): Promise<{
172
+ message: string;
173
+ }>;
174
+ generateSignedUrl(project: string, path: string, fileName: string, expiresIn?: number): Promise<string>;
175
+ }
176
+
177
+ interface S3Config {
178
+ region: string;
179
+ accessKeyId: string;
180
+ secretAccessKey: string;
181
+ bucketName: string;
182
+ }
183
+
184
+ declare class DomainError extends Error {
185
+ constructor(message: string);
186
+ }
187
+
188
+ declare const createS3Service: (config: S3Config) => S3Service;
189
+ declare const createS3ServiceFromEnv: (env?: NodeJS.ProcessEnv) => S3Service;
190
+
191
+ export { DomainError, type IS3Service, type S3Config, S3Service, createS3Service, createS3ServiceFromEnv };
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ // src/domain/errors/DomainError.ts
2
+ var DomainError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "DomainError";
6
+ }
7
+ };
8
+
9
+ // src/domain/value-objects/S3ObjectPath.ts
10
+ var normalizeSegment = (value, label) => {
11
+ const sanitized = value.trim().replace(/^\/+|\/+$/g, "");
12
+ if (!sanitized) {
13
+ throw new DomainError(`${label} is required`);
14
+ }
15
+ if (sanitized.includes("..")) {
16
+ throw new DomainError(`${label} cannot contain ".."`);
17
+ }
18
+ return sanitized;
19
+ };
20
+ var S3ObjectPath = class {
21
+ static build(project, path, fileName) {
22
+ const projectPart = normalizeSegment(project, "project");
23
+ const pathPart = normalizeSegment(path, "path");
24
+ if (!fileName) {
25
+ return `${projectPart}/${pathPart}/`;
26
+ }
27
+ const filePart = normalizeSegment(fileName, "fileName");
28
+ return `${projectPart}/${pathPart}/${filePart}`;
29
+ }
30
+ };
31
+
32
+ // src/application/use-cases/DeleteFileUseCase.ts
33
+ var DeleteFileUseCase = class {
34
+ constructor(gateway) {
35
+ this.gateway = gateway;
36
+ }
37
+ /**
38
+ * Executa a remoção de um arquivo.
39
+ *
40
+ * @param input Dados de identificação do arquivo.
41
+ * @returns Mensagem de confirmação de exclusão.
42
+ */
43
+ async execute(input) {
44
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
45
+ await this.gateway.deleteObject(objectKey);
46
+ return { message: "Arquivo deletado com sucesso" };
47
+ }
48
+ };
49
+
50
+ // src/application/use-cases/DownloadFileUseCase.ts
51
+ var DownloadFileUseCase = class {
52
+ constructor(gateway) {
53
+ this.gateway = gateway;
54
+ }
55
+ /**
56
+ * Executa o download de um arquivo.
57
+ *
58
+ * @param input Dados de identificação do arquivo.
59
+ * @returns Corpo do objeto retornado pelo SDK AWS (stream/blob).
60
+ */
61
+ async execute(input) {
62
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
63
+ return this.gateway.getObject(objectKey);
64
+ }
65
+ };
66
+
67
+ // src/application/use-cases/GenerateSignedUrlUseCase.ts
68
+ var GenerateSignedUrlUseCase = class {
69
+ constructor(gateway) {
70
+ this.gateway = gateway;
71
+ }
72
+ /**
73
+ * Executa a geração da URL assinada.
74
+ *
75
+ * Regra de negócio: `expiresIn` deve ser maior que zero.
76
+ *
77
+ * @param input Dados de identificação do arquivo e expiração opcional.
78
+ * @returns URL assinada para download temporário.
79
+ */
80
+ async execute(input) {
81
+ const expiresIn = input.expiresIn ?? 3600;
82
+ if (expiresIn <= 0) {
83
+ throw new DomainError("expiresIn must be greater than zero");
84
+ }
85
+ const objectKey = S3ObjectPath.build(input.project, input.path, input.fileName);
86
+ return this.gateway.generateGetSignedUrl(objectKey, expiresIn);
87
+ }
88
+ };
89
+
90
+ // src/application/use-cases/ListFilesUseCase.ts
91
+ var ListFilesUseCase = class {
92
+ constructor(gateway) {
93
+ this.gateway = gateway;
94
+ }
95
+ /**
96
+ * Executa a listagem de chaves no S3.
97
+ *
98
+ * @param input Dados de projeto e caminho.
99
+ * @returns Lista de chaves completas encontradas no bucket.
100
+ */
101
+ async execute(input) {
102
+ const prefix = S3ObjectPath.build(input.project, input.path);
103
+ return this.gateway.listObjects(prefix);
104
+ }
105
+ };
106
+
107
+ // src/facade/S3Service.ts
108
+ var S3Service = class {
109
+ constructor(downloadFileUseCase, listFilesUseCase, deleteFileUseCase, generateSignedUrlUseCase) {
110
+ this.downloadFileUseCase = downloadFileUseCase;
111
+ this.listFilesUseCase = listFilesUseCase;
112
+ this.deleteFileUseCase = deleteFileUseCase;
113
+ this.generateSignedUrlUseCase = generateSignedUrlUseCase;
114
+ }
115
+ async downloadFile(project, path, fileName) {
116
+ return this.downloadFileUseCase.execute({ project, path, fileName });
117
+ }
118
+ async listFiles(project, path) {
119
+ return this.listFilesUseCase.execute({ project, path });
120
+ }
121
+ async deleteFile(project, path, fileName) {
122
+ return this.deleteFileUseCase.execute({ project, path, fileName });
123
+ }
124
+ async generateSignedUrl(project, path, fileName, expiresIn = 10800) {
125
+ return this.generateSignedUrlUseCase.execute({
126
+ project,
127
+ path,
128
+ fileName,
129
+ expiresIn
130
+ });
131
+ }
132
+ };
133
+
134
+ // src/infrastructure/aws/AwsS3StorageGateway.ts
135
+ import {
136
+ DeleteObjectCommand,
137
+ GetObjectCommand,
138
+ ListObjectsV2Command,
139
+ S3Client
140
+ } from "@aws-sdk/client-s3";
141
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
142
+ var AwsS3StorageGateway = class {
143
+ constructor(config, client) {
144
+ this.config = config;
145
+ this.client = client ?? new S3Client({
146
+ region: config.region,
147
+ credentials: {
148
+ accessKeyId: config.accessKeyId,
149
+ secretAccessKey: config.secretAccessKey
150
+ }
151
+ });
152
+ }
153
+ client;
154
+ async getObject(objectKey) {
155
+ const { Body } = await this.client.send(
156
+ new GetObjectCommand({
157
+ Bucket: this.config.bucketName,
158
+ Key: objectKey
159
+ })
160
+ );
161
+ if (!Body) {
162
+ throw new Error(`Object "${objectKey}" returned empty body`);
163
+ }
164
+ return Body;
165
+ }
166
+ async listObjects(prefix) {
167
+ const { Contents } = await this.client.send(
168
+ new ListObjectsV2Command({
169
+ Bucket: this.config.bucketName,
170
+ Prefix: prefix
171
+ })
172
+ );
173
+ return Contents?.map((file) => file.Key).filter((key) => Boolean(key)) ?? [];
174
+ }
175
+ async deleteObject(objectKey) {
176
+ await this.client.send(
177
+ new DeleteObjectCommand({
178
+ Bucket: this.config.bucketName,
179
+ Key: objectKey
180
+ })
181
+ );
182
+ }
183
+ async generateGetSignedUrl(objectKey, expiresInSeconds) {
184
+ const command = new GetObjectCommand({
185
+ Bucket: this.config.bucketName,
186
+ Key: objectKey
187
+ });
188
+ return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });
189
+ }
190
+ };
191
+
192
+ // src/infrastructure/config/S3Config.ts
193
+ var createS3Config = (input) => {
194
+ const config = {
195
+ region: input.region?.trim() ?? "",
196
+ accessKeyId: input.accessKeyId?.trim() ?? "",
197
+ secretAccessKey: input.secretAccessKey?.trim() ?? "",
198
+ bucketName: input.bucketName?.trim() ?? ""
199
+ };
200
+ if (!config.region) throw new DomainError("AWS region is required");
201
+ if (!config.accessKeyId) throw new DomainError("AWS access key id is required");
202
+ if (!config.secretAccessKey) throw new DomainError("AWS secret access key is required");
203
+ if (!config.bucketName) throw new DomainError("AWS S3 bucket name is required");
204
+ return config;
205
+ };
206
+ var createS3ConfigFromEnv = (env = process.env) => {
207
+ return createS3Config({
208
+ region: env.AWS_REGION,
209
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
210
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
211
+ bucketName: env.AWS_S3_BUCKET_NAME
212
+ });
213
+ };
214
+
215
+ // src/index.ts
216
+ var buildService = (config) => {
217
+ const gateway = new AwsS3StorageGateway(config);
218
+ const downloadFileUseCase = new DownloadFileUseCase(gateway);
219
+ const listFilesUseCase = new ListFilesUseCase(gateway);
220
+ const deleteFileUseCase = new DeleteFileUseCase(gateway);
221
+ const generateSignedUrlUseCase = new GenerateSignedUrlUseCase(gateway);
222
+ return new S3Service(
223
+ downloadFileUseCase,
224
+ listFilesUseCase,
225
+ deleteFileUseCase,
226
+ generateSignedUrlUseCase
227
+ );
228
+ };
229
+ var createS3Service = (config) => {
230
+ return buildService(createS3Config(config));
231
+ };
232
+ var createS3ServiceFromEnv = (env = process.env) => {
233
+ return buildService(createS3ConfigFromEnv(env));
234
+ };
235
+ export {
236
+ DomainError,
237
+ S3Service,
238
+ createS3Service,
239
+ createS3ServiceFromEnv
240
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@vlsdev/s3-portable",
3
+ "version": "1.0.0",
4
+ "description": "Portable S3 service package for microservices.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build && npm run typecheck",
28
+ "publish:private": "npm publish",
29
+ "publish:private:otp": "npm publish --otp"
30
+ },
31
+ "dependencies": {
32
+ "@aws-sdk/client-s3": "^3.870.0",
33
+ "@aws-sdk/s3-request-presigner": "^3.870.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.13.10",
37
+ "tsup": "^8.4.0",
38
+ "typescript": "^5.8.2"
39
+ }
40
+ }