@wiicode/s3-client 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,294 @@
1
+ # @wiicode/s3-client
2
+
3
+ Official TypeScript/JavaScript SDK for WiiCode S3 Upload Service.
4
+
5
+ ## Installation
6
+
7
+ ### Option A: Install from Local Path (Development)
8
+
9
+ ```bash
10
+ npm install ../../path/to/wiis3.service/packages/client
11
+ ```
12
+
13
+ ### Option B: Install from Git Repository
14
+
15
+ ```bash
16
+ # Install from development branch
17
+ npm install git+https://github.com/eeyuub/wiis3.service.git#development:packages/client
18
+
19
+ # Or install from specific commit/tag
20
+ npm install git+https://github.com/eeyuub/wiis3.service.git#v1.0.0:packages/client
21
+ ```
22
+
23
+ ### Option C: Add to package.json
24
+
25
+ ```json
26
+ {
27
+ "dependencies": {
28
+ "@wiicode/s3-client": "git+https://github.com/eeyuub/wiis3.service.git#development:packages/client"
29
+ }
30
+ }
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```typescript
38
+ import { WiiS3Client } from '@wiicode/s3-client';
39
+
40
+ const s3 = new WiiS3Client({
41
+ endpoint: 'https://wiis3.wiicode.ma',
42
+ apiKey: 'wiicode_your_tenant_api_key',
43
+ });
44
+
45
+ // Upload a file
46
+ const file = await s3.upload(
47
+ buffer, // Buffer | Blob | File
48
+ 'photo.jpg', // filename
49
+ 'image/jpeg', // mimetype
50
+ {
51
+ userId: 'user-123',
52
+ metadata: { category: 'avatar' }
53
+ }
54
+ );
55
+
56
+ console.log(file.publicUrl);
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Usage Examples
62
+
63
+ ### Node.js (Backend)
64
+
65
+ ```typescript
66
+ import { WiiS3Client } from '@wiicode/s3-client';
67
+ import * as fs from 'fs';
68
+
69
+ const s3 = new WiiS3Client({
70
+ endpoint: process.env.WIIS3_ENDPOINT!,
71
+ apiKey: process.env.WIIS3_API_KEY!,
72
+ });
73
+
74
+ // Upload from file system
75
+ const buffer = fs.readFileSync('./image.jpg');
76
+ const file = await s3.upload(buffer, 'image.jpg', 'image/jpeg');
77
+
78
+ console.log('Uploaded:', file.publicUrl);
79
+
80
+ // Get file info
81
+ const info = await s3.getFile(file.id);
82
+ console.log('File size:', info.size);
83
+
84
+ // Get presigned download URL (expires in 1 hour)
85
+ const { url } = await s3.getDownloadUrl(file.id);
86
+ console.log('Download URL:', url);
87
+ ```
88
+
89
+ **Note:** Install `form-data` for Node.js:
90
+ ```bash
91
+ npm install form-data
92
+ ```
93
+
94
+ ### Browser/Frontend
95
+
96
+ ```typescript
97
+ import { WiiS3Client } from '@wiicode/s3-client';
98
+
99
+ const s3 = new WiiS3Client({
100
+ endpoint: 'https://wiis3.wiicode.ma',
101
+ apiKey: 'wiicode_your_tenant_api_key',
102
+ });
103
+
104
+ // Upload from file input
105
+ const fileInput = document.querySelector('input[type="file"]');
106
+ const file = fileInput.files[0];
107
+
108
+ const uploaded = await s3.upload(file, file.name, file.type);
109
+ console.log('Uploaded:', uploaded.publicUrl);
110
+
111
+ // Display the image
112
+ document.querySelector('img').src = uploaded.publicUrl;
113
+ ```
114
+
115
+ ### NestJS Integration
116
+
117
+ ```typescript
118
+ import { Injectable } from '@nestjs/common';
119
+ import { WiiS3Client } from '@wiicode/s3-client';
120
+ import { ConfigService } from '@nestjs/config';
121
+
122
+ @Injectable()
123
+ export class StorageService {
124
+ private s3Client: WiiS3Client;
125
+
126
+ constructor(private config: ConfigService) {
127
+ this.s3Client = new WiiS3Client({
128
+ endpoint: this.config.get('WIIS3_ENDPOINT'),
129
+ apiKey: this.config.get('WIIS3_API_KEY'),
130
+ });
131
+ }
132
+
133
+ async uploadFile(file: Express.Multer.File, userId: string) {
134
+ return await this.s3Client.upload(
135
+ file.buffer,
136
+ file.originalname,
137
+ file.mimetype,
138
+ { userId }
139
+ );
140
+ }
141
+
142
+ async getFileUrl(fileId: string) {
143
+ const file = await this.s3Client.getFile(fileId);
144
+ return file.publicUrl;
145
+ }
146
+ }
147
+ ```
148
+
149
+ ---
150
+
151
+ ## API Reference
152
+
153
+ ### Constructor
154
+
155
+ ```typescript
156
+ new WiiS3Client(config: WiiS3Config)
157
+ ```
158
+
159
+ **WiiS3Config:**
160
+ - `endpoint` (string, required) - API endpoint URL
161
+ - `apiKey` (string, required) - Tenant API key
162
+ - `timeout` (number, optional) - Request timeout in ms (default: 30000)
163
+
164
+ ### Methods
165
+
166
+ #### `upload(file, filename, mimetype, options?)`
167
+
168
+ Upload a file to S3.
169
+
170
+ **Parameters:**
171
+ - `file` (Buffer | Blob | File) - File data
172
+ - `filename` (string) - Original filename
173
+ - `mimetype` (string) - MIME type
174
+ - `options` (UploadOptions, optional)
175
+ - `userId` (string) - User identifier for organization
176
+ - `metadata` (object) - Custom JSON metadata
177
+
178
+ **Returns:** `Promise<UploadedFile>`
179
+
180
+ ```typescript
181
+ {
182
+ id: string;
183
+ originalName: string;
184
+ storedName: string;
185
+ mimeType: string;
186
+ size: number;
187
+ publicUrl: string;
188
+ uploadedAt: string;
189
+ }
190
+ ```
191
+
192
+ #### `getFile(fileId)`
193
+
194
+ Get file metadata and public URL.
195
+
196
+ **Parameters:**
197
+ - `fileId` (string) - File UUID
198
+
199
+ **Returns:** `Promise<FileInfo>`
200
+
201
+ ```typescript
202
+ {
203
+ id: string;
204
+ originalName: string;
205
+ storedName: string;
206
+ mimeType: string;
207
+ size: number;
208
+ publicUrl: string;
209
+ userId?: string;
210
+ metadata?: Record<string, any>;
211
+ uploadedAt: string;
212
+ }
213
+ ```
214
+
215
+ #### `getDownloadUrl(fileId)`
216
+
217
+ Generate a presigned download URL (valid for 1 hour).
218
+
219
+ **Parameters:**
220
+ - `fileId` (string) - File UUID
221
+
222
+ **Returns:** `Promise<DownloadUrlResponse>`
223
+
224
+ ```typescript
225
+ {
226
+ url: string;
227
+ expiresIn: number; // 3600 seconds
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Error Handling
234
+
235
+ ```typescript
236
+ import {
237
+ WiiS3Client,
238
+ AuthenticationError,
239
+ ValidationError,
240
+ NotFoundError,
241
+ QuotaExceededError,
242
+ NetworkError,
243
+ } from '@wiicode/s3-client';
244
+
245
+ try {
246
+ const file = await s3.upload(buffer, 'file.jpg', 'image/jpeg');
247
+ } catch (error) {
248
+ if (error instanceof AuthenticationError) {
249
+ console.error('Invalid API key');
250
+ } else if (error instanceof ValidationError) {
251
+ console.error('File validation failed:', error.message);
252
+ } else if (error instanceof QuotaExceededError) {
253
+ console.error('Storage quota exceeded');
254
+ } else if (error instanceof NotFoundError) {
255
+ console.error('File not found');
256
+ } else if (error instanceof NetworkError) {
257
+ console.error('Network error:', error.message);
258
+ }
259
+ }
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Environment Variables
265
+
266
+ ```env
267
+ WIIS3_ENDPOINT=https://wiis3.wiicode.ma
268
+ WIIS3_API_KEY=wiicode_your_tenant_api_key
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Obtaining API Keys
274
+
275
+ Contact your administrator to create a tenant and obtain an API key, or use the admin API:
276
+
277
+ ```bash
278
+ curl -X POST https://wiis3.wiicode.ma/admin/tenants \
279
+ -H "x-admin-key: your_admin_key" \
280
+ -H "Content-Type: application/json" \
281
+ -d '{"name": "my-project"}'
282
+ ```
283
+
284
+ ---
285
+
286
+ ## TypeScript Support
287
+
288
+ This package includes TypeScript definitions. No additional `@types` package needed.
289
+
290
+ ---
291
+
292
+ ## License
293
+
294
+ MIT
@@ -0,0 +1,31 @@
1
+ import { WiiS3Config, UploadOptions, UploadedFile, FileInfo, DownloadUrlResponse } from './types';
2
+ export declare class WiiS3Client {
3
+ private readonly endpoint;
4
+ private readonly apiKey;
5
+ private readonly timeout;
6
+ constructor(config: WiiS3Config);
7
+ /**
8
+ * Upload a file to WiiS3
9
+ * @param file - File buffer, Blob, or File object
10
+ * @param filename - Original filename
11
+ * @param mimetype - MIME type of the file
12
+ * @param options - Optional upload options (userId, metadata)
13
+ * @returns Uploaded file information including public URL
14
+ */
15
+ upload(file: Buffer | Blob | File, filename: string, mimetype: string, options?: UploadOptions): Promise<UploadedFile>;
16
+ /**
17
+ * Get file metadata and public URL
18
+ * @param fileId - UUID of the file
19
+ * @returns File information including public URL
20
+ */
21
+ getFile(fileId: string): Promise<FileInfo>;
22
+ /**
23
+ * Get presigned download URL (valid for 1 hour)
24
+ * @param fileId - UUID of the file
25
+ * @returns Presigned URL and expiry time
26
+ */
27
+ getDownloadUrl(fileId: string): Promise<DownloadUrlResponse>;
28
+ private createFormData;
29
+ private request;
30
+ private handleErrorResponse;
31
+ }
package/dist/client.js ADDED
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WiiS3Client = void 0;
4
+ const errors_1 = require("./errors");
5
+ class WiiS3Client {
6
+ constructor(config) {
7
+ if (!config.endpoint) {
8
+ throw new errors_1.ValidationError('endpoint is required');
9
+ }
10
+ if (!config.apiKey) {
11
+ throw new errors_1.ValidationError('apiKey is required');
12
+ }
13
+ this.endpoint = config.endpoint.replace(/\/$/, '');
14
+ this.apiKey = config.apiKey;
15
+ this.timeout = config.timeout || 30000;
16
+ }
17
+ /**
18
+ * Upload a file to WiiS3
19
+ * @param file - File buffer, Blob, or File object
20
+ * @param filename - Original filename
21
+ * @param mimetype - MIME type of the file
22
+ * @param options - Optional upload options (userId, metadata)
23
+ * @returns Uploaded file information including public URL
24
+ */
25
+ async upload(file, filename, mimetype, options) {
26
+ const formData = await this.createFormData(file, filename, mimetype, options);
27
+ const response = await this.request('/api/v1/upload', {
28
+ method: 'POST',
29
+ body: formData,
30
+ });
31
+ if (!response.success || !response.file) {
32
+ throw new errors_1.WiiS3Error(response.error || 'Upload failed');
33
+ }
34
+ return response.file;
35
+ }
36
+ /**
37
+ * Get file metadata and public URL
38
+ * @param fileId - UUID of the file
39
+ * @returns File information including public URL
40
+ */
41
+ async getFile(fileId) {
42
+ const response = await this.request(`/api/v1/files/${fileId}`, { method: 'GET' });
43
+ if (!response.success || !response.file) {
44
+ throw new errors_1.NotFoundError(response.error || 'File not found');
45
+ }
46
+ return response.file;
47
+ }
48
+ /**
49
+ * Get presigned download URL (valid for 1 hour)
50
+ * @param fileId - UUID of the file
51
+ * @returns Presigned URL and expiry time
52
+ */
53
+ async getDownloadUrl(fileId) {
54
+ const response = await this.request(`/api/v1/files/${fileId}/download`, { method: 'GET' });
55
+ if (!response.success || !response.url) {
56
+ throw new errors_1.NotFoundError(response.error || 'File not found');
57
+ }
58
+ return {
59
+ url: response.url,
60
+ expiresIn: response.expiresIn || 3600,
61
+ };
62
+ }
63
+ async createFormData(file, filename, mimetype, options) {
64
+ // Browser environment
65
+ if (typeof window !== 'undefined' && typeof FormData !== 'undefined') {
66
+ const formData = new FormData();
67
+ if (file instanceof Blob || file instanceof File) {
68
+ formData.append('file', file, filename);
69
+ }
70
+ else {
71
+ // Buffer in browser - convert to Blob
72
+ const blob = new Blob([file], { type: mimetype });
73
+ formData.append('file', blob, filename);
74
+ }
75
+ if (options?.userId) {
76
+ formData.append('userId', options.userId);
77
+ }
78
+ if (options?.metadata) {
79
+ formData.append('metadata', JSON.stringify(options.metadata));
80
+ }
81
+ return formData;
82
+ }
83
+ // Node.js environment
84
+ try {
85
+ const FormDataNode = require('form-data');
86
+ const formData = new FormDataNode();
87
+ if (Buffer.isBuffer(file)) {
88
+ formData.append('file', file, {
89
+ filename,
90
+ contentType: mimetype,
91
+ });
92
+ }
93
+ else {
94
+ // Blob/File in Node - convert to Buffer
95
+ const arrayBuffer = await file.arrayBuffer();
96
+ formData.append('file', Buffer.from(arrayBuffer), {
97
+ filename,
98
+ contentType: mimetype,
99
+ });
100
+ }
101
+ if (options?.userId) {
102
+ formData.append('userId', options.userId);
103
+ }
104
+ if (options?.metadata) {
105
+ formData.append('metadata', JSON.stringify(options.metadata));
106
+ }
107
+ return formData;
108
+ }
109
+ catch (e) {
110
+ throw new errors_1.WiiS3Error('form-data package is required in Node.js. Install it with: npm install form-data', 500, 'MISSING_DEPENDENCY');
111
+ }
112
+ }
113
+ async request(path, options) {
114
+ const url = `${this.endpoint}${path}`;
115
+ const headers = {
116
+ 'x-api-key': this.apiKey,
117
+ };
118
+ // Handle form-data headers in Node.js
119
+ if (options.body && typeof options.body.getHeaders === 'function') {
120
+ Object.assign(headers, options.body.getHeaders());
121
+ }
122
+ const controller = new AbortController();
123
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
124
+ try {
125
+ let fetchFn;
126
+ // Use native fetch if available, otherwise try node-fetch
127
+ if (typeof fetch !== 'undefined') {
128
+ fetchFn = fetch;
129
+ }
130
+ else {
131
+ try {
132
+ fetchFn = require('node-fetch');
133
+ }
134
+ catch {
135
+ // Node 18+ has built-in fetch
136
+ fetchFn = globalThis.fetch;
137
+ }
138
+ }
139
+ const response = await fetchFn(url, {
140
+ ...options,
141
+ headers,
142
+ signal: controller.signal,
143
+ });
144
+ clearTimeout(timeoutId);
145
+ const data = await response.json();
146
+ if (!response.ok) {
147
+ this.handleErrorResponse(response.status, data);
148
+ }
149
+ return data;
150
+ }
151
+ catch (error) {
152
+ clearTimeout(timeoutId);
153
+ if (error.name === 'AbortError') {
154
+ throw new errors_1.NetworkError('Request timeout');
155
+ }
156
+ if (error instanceof errors_1.WiiS3Error) {
157
+ throw error;
158
+ }
159
+ throw new errors_1.NetworkError(error.message || 'Network error occurred');
160
+ }
161
+ }
162
+ handleErrorResponse(status, data) {
163
+ const message = data?.message || data?.error || 'Unknown error';
164
+ switch (status) {
165
+ case 401:
166
+ throw new errors_1.AuthenticationError(message);
167
+ case 404:
168
+ throw new errors_1.NotFoundError(message);
169
+ case 400:
170
+ if (message.toLowerCase().includes('quota')) {
171
+ throw new errors_1.ValidationError(message);
172
+ }
173
+ throw new errors_1.ValidationError(message);
174
+ default:
175
+ throw new errors_1.WiiS3Error(message, status);
176
+ }
177
+ }
178
+ }
179
+ exports.WiiS3Client = WiiS3Client;
180
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFRQSxxQ0FNa0I7QUFFbEIsTUFBYSxXQUFXO0lBS3RCLFlBQVksTUFBbUI7UUFDN0IsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNyQixNQUFNLElBQUksd0JBQWUsQ0FBQyxzQkFBc0IsQ0FBQyxDQUFDO1FBQ3BELENBQUM7UUFDRCxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDO1lBQ25CLE1BQU0sSUFBSSx3QkFBZSxDQUFDLG9CQUFvQixDQUFDLENBQUM7UUFDbEQsQ0FBQztRQUVELElBQUksQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBQ25ELElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUM1QixJQUFJLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxPQUFPLElBQUksS0FBSyxDQUFDO0lBQ3pDLENBQUM7SUFFRDs7Ozs7OztPQU9HO0lBQ0gsS0FBSyxDQUFDLE1BQU0sQ0FDVixJQUEwQixFQUMxQixRQUFnQixFQUNoQixRQUFnQixFQUNoQixPQUF1QjtRQUV2QixNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsSUFBSSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFFOUUsTUFBTSxRQUFRLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUNqQyxnQkFBZ0IsRUFDaEI7WUFDRSxNQUFNLEVBQUUsTUFBTTtZQUNkLElBQUksRUFBRSxRQUFRO1NBQ2YsQ0FDRixDQUFDO1FBRUYsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDeEMsTUFBTSxJ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QU8sS0FBVSxFQUFFLENBQUM7WUFDcEIsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBRXhCLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxZQUFZLEVBQUUsQ0FBQztnQkFDaEMsTUFBTSxJQUFJLHFCQUFZLENBQUMsaUJBQWlCLENBQUMsQ0FBQztZQUM1QyxDQUFDO1lBRUQsSUFBSSxLQUFLLFlBQVksbUJBQVUsRUFBRSxDQUFDO2dCQUNoQyxNQUFNLEtBQUssQ0FBQztZQUNkLENBQUM7WUFFRCxNQUFNLElBQUkscUJBQVksQ0FBQyxLQUFLLENBQUMsT0FBTyxJQUFJLHdCQUF3QixDQUFDLENBQUM7UUFDcEUsQ0FBQztJQUNILENBQUM7SUFFTyxtQkFBbUIsQ0FBQyxNQUFjLEVBQUUsSUFBUztRQUNuRCxNQUFNLE9BQU8sR0FBRyxJQUFJLEVBQUUsT0FBTyxJQUFJLElBQUksRUFBRSxLQUFLLElBQUksZUFBZSxDQUFDO1FBRWhFLFFBQVEsTUFBTSxFQUFFLENBQUM7WUFDZixLQUFLLEdBQUc7Z0JBQ04sTUFBTSxJQUFJLDRCQUFtQixDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQ3pDLEtBQUssR0FBRztnQkFDTixNQUFNLElBQUksc0JBQWEsQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUNuQyxLQUFLLEdBQUc7Z0JBQ04sSUFBSSxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7b0JBQzVDLE1BQU0sSUFBSSx3QkFBZSxDQUFDLE9BQU8sQ0FBQyxDQUFDO2dCQUNyQyxDQUFDO2dCQUNELE1BQU0sSUFBSSx3QkFBZSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQ3JDO2dCQUNFLE1BQU0sSUFBSSxtQkFBVSxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQztRQUMxQyxDQUFDO0lBQ0gsQ0FBQztDQUNGO0FBcE9ELGtDQW9PQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIFdpaVMzQ29uZmlnLFxuICBVcGxvYWRPcHRpb25zLFxuICBVcGxvYWRlZEZpbGUsXG4gIEZpbGVJbmZvLFxuICBEb3dubG9hZFVybFJlc3BvbnNlLFxuICBBcGlSZXNwb25zZSxcbn0gZnJvbSAnLi90eXBlcyc7XG5pbXBvcnQge1xuICBXaWlTM0Vycm9yLFxuICBBdXRoZW50aWNhdGlvbkVycm9yLFxuICBWYWxpZGF0aW9uRXJyb3IsXG4gIE5vdEZvdW5kRXJyb3IsXG4gIE5ldHdvcmtFcnJvcixcbn0gZnJvbSAnLi9lcnJvcnMnO1xuXG5leHBvcnQgY2xhc3MgV2lpUzNDbGllbnQge1xuICBwcml2YXRlIHJlYWRvbmx5IGVuZHBvaW50OiBzdHJpbmc7XG4gIHByaXZhdGUgcmVhZG9ubHkgYXBpS2V5OiBzdHJpbmc7XG4gIHByaXZhdGUgcmVhZG9ubHkgdGltZW91dDogbnVtYmVyO1xuXG4gIGNvbnN0cnVjdG9yKGNvbmZpZzogV2lpUzNDb25maWcpIHtcbiAgICBpZiAoIWNvbmZpZy5lbmRwb2ludCkge1xuICAgICAgdGhyb3cgbmV3IFZhbGlkYXRpb25FcnJvcignZW5kcG9pbnQgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG4gICAgaWYgKCFjb25maWcuYXBpS2V5KSB7XG4gICAgICB0aHJvdyBuZXcgVmFsaWRhdGlvbkVycm9yKCdhcGlLZXkgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG5cbiAgICB0aGlzLmVuZHBvaW50ID0gY29uZmlnLmVuZHBvaW50LnJlcGxhY2UoL1xcLyQvLCAnJyk7XG4gICAgdGhpcy5hcGlLZXkgPSBjb25maWcuYXBpS2V5O1xuICAgIHRoaXMudGltZW91dCA9IGNvbmZpZy50aW1lb3V0IHx8IDMwMDAwO1xuICB9XG5cbiAgLyoqXG4gICAqIFVwbG9hZCBhIGZpbGUgdG8gV2lpUzNcbiAgICogQHBhcmFtIGZpbGUgLSBGaWxlIGJ1ZmZlciwgQmxvYiwgb3IgRmlsZSBvYmplY3RcbiAgICogQHBhcmFtIGZpbGVuYW1lIC0gT3JpZ2luYWwgZmlsZW5hbWVcbiAgICogQHBhcmFtIG1pbWV0eXBlIC0gTUlNRSB0eXBlIG9mIHRoZSBmaWxlXG4gICAqIEBwYXJhbSBvcHRpb25zIC0gT3B0aW9uYWwgdXBsb2FkIG9wdGlvbnMgKHVzZXJJZCwgbWV0YWRhdGEpXG4gICAqIEByZXR1cm5zIFVwbG9hZGVkIGZpbGUgaW5mb3JtYXRpb24gaW5jbHVkaW5nIHB1YmxpYyBVUkxcbiAgICovXG4gIGFzeW5jIHVwbG9hZChcbiAgICBmaWxlOiBCdWZmZXIgfCBCbG9iIHwgRmlsZSxcbiAgICBmaWxlbmFtZTogc3RyaW5nLFxuICAgIG1pbWV0eXBlOiBzdHJpbmcsXG4gICAgb3B0aW9ucz86IFVwbG9hZE9wdGlvbnMsXG4gICk6IFByb21pc2U8VXBsb2FkZWRGaWxlPiB7XG4gICAgY29uc3QgZm9ybURhdGEgPSBhd2FpdCB0aGlzLmNyZWF0ZUZvcm1EYXRhKGZpbGUsIGZpbGVuYW1lLCBtaW1ldHlwZSwgb3B0aW9ucyk7XG5cbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IHRoaXMucmVxdWVzdDxBcGlSZXNwb25zZTxVcGxvYWRlZEZpbGU+PihcbiAgICAgICcvYXBpL3YxL3VwbG9hZCcsXG4gICAgICB7XG4gICAgICAgIG1ldGhvZDogJ1BPU1QnLFxuICAgICAgICBib2R5OiBmb3JtRGF0YSxcbiAgICAgIH0sXG4gICAgKTtcblxuICAgIGlmICghcmVzcG9uc2Uuc3VjY2VzcyB8fCAhcmVzcG9uc2UuZmlsZSkge1xuICAgICAgdGhyb3cgbmV3IFdpaVMzRXJyb3IocmVzcG9uc2UuZXJyb3IgfHwgJ1VwbG9hZCBmYWlsZWQnKTtcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzcG9uc2UuZmlsZTtcbiAgfVxuXG4gIC8qKlxuICAgKiBHZXQgZmlsZSBtZXRhZGF0YSBhbmQgcHVibGljIFVSTFxuICAgKiBAcGFyYW0gZmlsZUlkIC0gVVVJRCBvZiB0aGUgZmlsZVxuICAgKiBAcmV0dXJucyBGaWxlIGluZm9ybWF0aW9uIGluY2x1ZGluZyBwdWJsaWMgVVJMXG4gICAqL1xuICBhc3luYyBnZXRGaWxlKGZpbGVJZDogc3RyaW5nKTogUHJvbWlzZTxGaWxlSW5mbz4ge1xuICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgdGhpcy5yZXF1ZXN0PEFwaVJlc3BvbnNlPEZpbGVJbmZvPj4oXG4gICAgICBgL2FwaS92MS9maWxlcy8ke2ZpbGVJZH1gLFxuICAgICAgeyBtZXRob2Q6ICdHRVQnIH0sXG4gICAgKTtcblxuICAgIGlmICghcmVzcG9uc2Uuc3VjY2VzcyB8fCAhcmVzcG9uc2UuZmlsZSkge1xuICAgICAgdGhyb3cgbmV3IE5vdEZvdW5kRXJyb3IocmVzcG9uc2UuZXJyb3IgfHwgJ0ZpbGUgbm90IGZvdW5kJyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHJlc3BvbnNlLmZpbGU7XG4gIH1cblxuICAvKipcbiAgICogR2V0IHByZXNpZ25lZCBkb3dubG9hZCBVUkwgKHZhbGlkIGZvciAxIGhvdXIpXG4gICAqIEBwYXJhbSBmaWxlSWQgLSBVVUlEIG9mIHRoZSBmaWxlXG4gICAqIEByZXR1cm5zIFByZXNpZ25lZCBVUkwgYW5kIGV4cGlyeSB0aW1lXG4gICAqL1xuICBhc3luYyBnZXREb3dubG9hZFVybChmaWxlSWQ6IHN0cmluZyk6IFByb21pc2U8RG93bmxvYWRVcmxSZXNwb25zZT4ge1xuICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgdGhpcy5yZXF1ZXN0PEFwaVJlc3BvbnNlPHZvaWQ+PihcbiAgICAgIGAvYXBpL3YxL2ZpbGVzLyR7ZmlsZUlkfS9kb3dubG9hZGAsXG4gICAgICB7IG1ldGhvZDogJ0dFVCcgfSxcbiAgICApO1xuXG4gICAgaWYgKCFyZXNwb25zZS5zdWNjZXNzIHx8ICFyZXNwb25zZS51cmwpIHtcbiAgICAgIHRocm93IG5ldyBOb3RGb3VuZEVycm9yKHJlc3BvbnNlLmVycm9yIHx8ICdGaWxlIG5vdCBmb3VuZCcpO1xuICAgIH1cblxuICAgIHJldHVybiB7XG4gICAgICB1cmw6IHJlc3BvbnNlLnVybCxcbiAgICAgIGV4cGlyZXNJbjogcmVzcG9uc2UuZXhwaXJlc0luIHx8IDM2MDAsXG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgY3JlYXRlRm9ybURhdGEoXG4gICAgZmlsZTogQnVmZmVyIHwgQmxvYiB8IEZpbGUsXG4gICAgZmlsZW5hbWU6IHN0cmluZyxcbiAgICBtaW1ldHlwZTogc3RyaW5nLFxuICAgIG9wdGlvbnM/OiBVcGxvYWRPcHRpb25zLFxuICApOiBQcm9taXNlPEZvcm1EYXRhIHwgYW55PiB7XG4gICAgLy8gQnJvd3NlciBlbnZpcm9ubWVudFxuICAgIGlmICh0eXBlb2Ygd2luZG93ICE9PSAndW5kZWZpbmVkJyAmJiB0eXBlb2YgRm9ybURhdGEgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICBjb25zdCBmb3JtRGF0YSA9IG5ldyBGb3JtRGF0YSgpO1xuXG4gICAgICBpZiAoZmlsZSBpbnN0YW5jZW9mIEJsb2IgfHwgZmlsZSBpbnN0YW5jZW9mIEZpbGUpIHtcbiAgICAgICAgZm9ybURhdGEuYXBwZW5kKCdmaWxlJywgZmlsZSwgZmlsZW5hbWUpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8gQnVmZmVyIGluIGJyb3dzZXIgLSBjb252ZXJ0IHRvIEJsb2JcbiAgICAgICAgY29uc3QgYmxvYiA9IG5ldyBCbG9iKFtmaWxlIGFzIGFueV0sIHsgdHlwZTogbWltZXR5cGUgfSk7XG4gICAgICAgIGZvcm1EYXRhLmFwcGVuZCgnZmlsZScsIGJsb2IsIGZpbGVuYW1lKTtcbiAgICAgIH1cblxuICAgICAgaWYgKG9wdGlvbnM/LnVzZXJJZCkge1xuICAgICAgICBmb3JtRGF0YS5hcHBlbmQoJ3VzZXJJZCcsIG9wdGlvbnMudXNlcklkKTtcbiAgICAgIH1cbiAgICAgIGlmIChvcHRpb25zPy5tZXRhZGF0YSkge1xuICAgICAgICBmb3JtRGF0YS5hcHBlbmQoJ21ldGFkYXRhJywgSlNPTi5zdHJpbmdpZnkob3B0aW9ucy5tZXRhZGF0YSkpO1xuICAgICAgfVxuXG4gICAgICByZXR1cm4gZm9ybURhdGE7XG4gICAgfVxuXG4gICAgLy8gTm9kZS5qcyBlbnZpcm9ubWVudFxuICAgIHRyeSB7XG4gICAgICBjb25zdCBGb3JtRGF0YU5vZGUgPSByZXF1aXJlKCdmb3JtLWRhdGEnKTtcbiAgICAgIGNvbnN0IGZvcm1EYXRhID0gbmV3IEZvcm1EYXRhTm9kZSgpO1xuXG4gICAgICBpZiAoQnVmZmVyLmlzQnVmZmVyKGZpbGUpKSB7XG4gICAgICAgIGZvcm1EYXRhLmFwcGVuZCgnZmlsZScsIGZpbGUsIHtcbiAgICAgICAgICBmaWxlbmFtZSxcbiAgICAgICAgICBjb250ZW50VHlwZTogbWltZXR5cGUsXG4gICAgICAgIH0pO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8gQmxvYi9GaWxlIGluIE5vZGUgLSBjb252ZXJ0IHRvIEJ1ZmZlclxuICAgICAgICBjb25zdCBhcnJheUJ1ZmZlciA9IGF3YWl0IChmaWxlIGFzIEJsb2IpLmFycmF5QnVmZmVyKCk7XG4gICAgICAgIGZvcm1EYXRhLmFwcGVuZCgnZmlsZScsIEJ1ZmZlci5mcm9tKGFycmF5QnVmZmVyKSwge1xuICAgICAgICAgIGZpbGVuYW1lLFxuICAgICAgICAgIGNvbnRlbnRUeXBlOiBtaW1ldHlwZSxcbiAgICAgICAgfSk7XG4gICAgICB9XG5cbiAgICAgIGlmIChvcHRpb25zPy51c2VySWQpIHtcbiAgICAgICAgZm9ybURhdGEuYXBwZW5kKCd1c2VySWQnLCBvcHRpb25zLnVzZXJJZCk7XG4gICAgICB9XG4gICAgICBpZiAob3B0aW9ucz8ubWV0YWRhdGEpIHtcbiAgICAgICAgZm9ybURhdGEuYXBwZW5kKCdtZXRhZGF0YScsIEpTT04uc3RyaW5naWZ5KG9wdGlvbnMubWV0YWRhdGEpKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIGZvcm1EYXRhO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIHRocm93IG5ldyBXaWlTM0Vycm9yKFxuICAgICAgICAnZm9ybS1kYXRhIHBhY2thZ2UgaXMgcmVxdWlyZWQgaW4gTm9kZS5qcy4gSW5zdGFsbCBpdCB3aXRoOiBucG0gaW5zdGFsbCBmb3JtLWRhdGEnLFxuICAgICAgICA1MDAsXG4gICAgICAgICdNSVNTSU5HX0RFUEVOREVOQ1knLFxuICAgICAgKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGFzeW5jIHJlcXVlc3Q8VD4ocGF0aDogc3RyaW5nLCBvcHRpb25zOiBSZXF1ZXN0SW5pdCAmIHsgYm9keT86IGFueSB9KTogUHJvbWlzZTxUPiB7XG4gICAgY29uc3QgdXJsID0gYCR7dGhpcy5lbmRwb2ludH0ke3BhdGh9YDtcbiAgICBjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge1xuICAgICAgJ3gtYXBpLWtleSc6IHRoaXMuYXBpS2V5LFxuICAgIH07XG5cbiAgICAvLyBIYW5kbGUgZm9ybS1kYXRhIGhlYWRlcnMgaW4gTm9kZS5qc1xuICAgIGlmIChvcHRpb25zLmJvZHkgJiYgdHlwZW9mIG9wdGlvbnMuYm9keS5nZXRIZWFkZXJzID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICBPYmplY3QuYXNzaWduKGhlYWRlcnMsIG9wdGlvbnMuYm9keS5nZXRIZWFkZXJzKCkpO1xuICAgIH1cblxuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBuZXcgQWJvcnRDb250cm9sbGVyKCk7XG4gICAgY29uc3QgdGltZW91dElkID0gc2V0VGltZW91dCgoKSA9PiBjb250cm9sbGVyLmFib3J0KCksIHRoaXMudGltZW91dCk7XG5cbiAgICB0cnkge1xuICAgICAgbGV0IGZldGNoRm46IHR5cGVvZiBmZXRjaDtcblxuICAgICAgLy8gVXNlIG5hdGl2ZSBmZXRjaCBpZiBhdmFpbGFibGUsIG90aGVyd2lzZSB0cnkgbm9kZS1mZXRjaFxuICAgICAgaWYgKHR5cGVvZiBmZXRjaCAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgZmV0Y2hGbiA9IGZldGNoO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBmZXRjaEZuID0gcmVxdWlyZSgnbm9kZS1mZXRjaCcpO1xuICAgICAgICB9IGNhdGNoIHtcbiAgICAgICAgICAvLyBOb2RlIDE4KyBoYXMgYnVpbHQtaW4gZmV0Y2hcbiAgICAgICAgICBmZXRjaEZuID0gZ2xvYmFsVGhpcy5mZXRjaDtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZldGNoRm4odXJsLCB7XG4gICAgICAgIC4uLm9wdGlvbnMsXG4gICAgICAgIGhlYWRlcnMsXG4gICAgICAgIHNpZ25hbDogY29udHJvbGxlci5zaWduYWwsXG4gICAgICB9KTtcblxuICAgICAgY2xlYXJUaW1lb3V0KHRpbWVvdXRJZCk7XG5cbiAgICAgIGNvbnN0IGRhdGEgPSBhd2FpdCByZXNwb25zZS5qc29uKCk7XG5cbiAgICAgIGlmICghcmVzcG9uc2Uub2spIHtcbiAgICAgICAgdGhpcy5oYW5kbGVFcnJvclJlc3BvbnNlKHJlc3BvbnNlLnN0YXR1cywgZGF0YSk7XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBkYXRhIGFzIFQ7XG4gICAgfSBjYXRjaCAoZXJyb3I6IGFueSkge1xuICAgICAgY2xlYXJUaW1lb3V0KHRpbWVvdXRJZCk7XG5cbiAgICAgIGlmIChlcnJvci5uYW1lID09PSAnQWJvcnRFcnJvcicpIHtcbiAgICAgICAgdGhyb3cgbmV3IE5ldHdvcmtFcnJvcignUmVxdWVzdCB0aW1lb3V0Jyk7XG4gICAgICB9XG5cbiAgICAgIGlmIChlcnJvciBpbnN0YW5jZW9mIFdpaVMzRXJyb3IpIHtcbiAgICAgICAgdGhyb3cgZXJyb3I7XG4gICAgICB9XG5cbiAgICAgIHRocm93IG5ldyBOZXR3b3JrRXJyb3IoZXJyb3IubWVzc2FnZSB8fCAnTmV0d29yayBlcnJvciBvY2N1cnJlZCcpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlRXJyb3JSZXNwb25zZShzdGF0dXM6IG51bWJlciwgZGF0YTogYW55KTogbmV2ZXIge1xuICAgIGNvbnN0IG1lc3NhZ2UgPSBkYXRhPy5tZXNzYWdlIHx8IGRhdGE/LmVycm9yIHx8ICdVbmtub3duIGVycm9yJztcblxuICAgIHN3aXRjaCAoc3RhdHVzKSB7XG4gICAgICBjYXNlIDQwMTpcbiAgICAgICAgdGhyb3cgbmV3IEF1dGhlbnRpY2F0aW9uRXJyb3IobWVzc2FnZSk7XG4gICAgICBjYXNlIDQwNDpcbiAgICAgICAgdGhyb3cgbmV3IE5vdEZvdW5kRXJyb3IobWVzc2FnZSk7XG4gICAgICBjYXNlIDQwMDpcbiAgICAgICAgaWYgKG1lc3NhZ2UudG9Mb3dlckNhc2UoKS5pbmNsdWRlcygncXVvdGEnKSkge1xuICAgICAgICAgIHRocm93IG5ldyBWYWxpZGF0aW9uRXJyb3IobWVzc2FnZSk7XG4gICAgICAgIH1cbiAgICAgICAgdGhyb3cgbmV3IFZhbGlkYXRpb25FcnJvcihtZXNzYWdlKTtcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRocm93IG5ldyBXaWlTM0Vycm9yKG1lc3NhZ2UsIHN0YXR1cyk7XG4gICAgfVxuICB9XG59XG4iXX0=
@@ -0,0 +1,20 @@
1
+ export declare class WiiS3Error extends Error {
2
+ readonly statusCode: number;
3
+ readonly code: string;
4
+ constructor(message: string, statusCode?: number, code?: string);
5
+ }
6
+ export declare class AuthenticationError extends WiiS3Error {
7
+ constructor(message?: string);
8
+ }
9
+ export declare class ValidationError extends WiiS3Error {
10
+ constructor(message: string);
11
+ }
12
+ export declare class NotFoundError extends WiiS3Error {
13
+ constructor(message?: string);
14
+ }
15
+ export declare class QuotaExceededError extends WiiS3Error {
16
+ constructor(message?: string);
17
+ }
18
+ export declare class NetworkError extends WiiS3Error {
19
+ constructor(message?: string);
20
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkError = exports.QuotaExceededError = exports.NotFoundError = exports.ValidationError = exports.AuthenticationError = exports.WiiS3Error = void 0;
4
+ class WiiS3Error extends Error {
5
+ constructor(message, statusCode = 500, code = 'UNKNOWN_ERROR') {
6
+ super(message);
7
+ this.name = 'WiiS3Error';
8
+ this.statusCode = statusCode;
9
+ this.code = code;
10
+ }
11
+ }
12
+ exports.WiiS3Error = WiiS3Error;
13
+ class AuthenticationError extends WiiS3Error {
14
+ constructor(message = 'Invalid or missing API key') {
15
+ super(message, 401, 'AUTHENTICATION_ERROR');
16
+ this.name = 'AuthenticationError';
17
+ }
18
+ }
19
+ exports.AuthenticationError = AuthenticationError;
20
+ class ValidationError extends WiiS3Error {
21
+ constructor(message) {
22
+ super(message, 400, 'VALIDATION_ERROR');
23
+ this.name = 'ValidationError';
24
+ }
25
+ }
26
+ exports.ValidationError = ValidationError;
27
+ class NotFoundError extends WiiS3Error {
28
+ constructor(message = 'File not found') {
29
+ super(message, 404, 'NOT_FOUND');
30
+ this.name = 'NotFoundError';
31
+ }
32
+ }
33
+ exports.NotFoundError = NotFoundError;
34
+ class QuotaExceededError extends WiiS3Error {
35
+ constructor(message = 'Storage quota exceeded') {
36
+ super(message, 400, 'QUOTA_EXCEEDED');
37
+ this.name = 'QuotaExceededError';
38
+ }
39
+ }
40
+ exports.QuotaExceededError = QuotaExceededError;
41
+ class NetworkError extends WiiS3Error {
42
+ constructor(message = 'Network error occurred') {
43
+ super(message, 0, 'NETWORK_ERROR');
44
+ this.name = 'NetworkError';
45
+ }
46
+ }
47
+ exports.NetworkError = NetworkError;
48
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxNQUFhLFVBQVcsU0FBUSxLQUFLO0lBSW5DLFlBQVksT0FBZSxFQUFFLGFBQXFCLEdBQUcsRUFBRSxPQUFlLGVBQWU7UUFDbkYsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2YsSUFBSSxDQUFDLElBQUksR0FBRyxZQUFZLENBQUM7UUFDekIsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUM7UUFDN0IsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztDQUNGO0FBVkQsZ0NBVUM7QUFFRCxNQUFhLG1CQUFvQixTQUFRLFVBQVU7SUFDakQsWUFBWSxVQUFrQiw0QkFBNEI7UUFDeEQsS0FBSyxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsc0JBQXNCLENBQUMsQ0FBQztRQUM1QyxJQUFJLENBQUMsSUFBSSxHQUFHLHFCQUFxQixDQUFDO0lBQ3BDLENBQUM7Q0FDRjtBQUxELGtEQUtDO0FBRUQsTUFBYSxlQUFnQixTQUFRLFVBQVU7SUFDN0MsWUFBWSxPQUFlO1FBQ3pCLEtBQUssQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLGtCQUFrQixDQUFDLENBQUM7UUFDeEMsSUFBSSxDQUFDLElBQUksR0FBRyxpQkFBaUIsQ0FBQztJQUNoQyxDQUFDO0NBQ0Y7QUFMRCwwQ0FLQztBQUVELE1BQWEsYUFBYyxTQUFRLFVBQVU7SUFDM0MsWUFBWSxVQUFrQixnQkFBZ0I7UUFDNUMsS0FBSyxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsV0FBVyxDQUFDLENBQUM7UUFDakMsSUFBSSxDQUFDLElBQUksR0FBRyxlQUFlLENBQUM7SUFDOUIsQ0FBQztDQUNGO0FBTEQsc0NBS0M7QUFFRCxNQUFhLGtCQUFtQixTQUFRLFVBQVU7SUFDaEQsWUFBWSxVQUFrQix3QkFBd0I7UUFDcEQsS0FBSyxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQztRQUN0QyxJQUFJLENBQUMsSUFBSSxHQUFHLG9CQUFvQixDQUFDO0lBQ25DLENBQUM7Q0FDRjtBQUxELGdEQUtDO0FBRUQsTUFBYSxZQUFhLFNBQVEsVUFBVTtJQUMxQyxZQUFZLFVBQWtCLHdCQUF3QjtRQUNwRCxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsRUFBRSxlQUFlLENBQUMsQ0FBQztRQUNuQyxJQUFJLENBQUMsSUFBSSxHQUFHLGNBQWMsQ0FBQztJQUM3QixDQUFDO0NBQ0Y7QUFMRCxvQ0FLQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjbGFzcyBXaWlTM0Vycm9yIGV4dGVuZHMgRXJyb3Ige1xuICBwdWJsaWMgcmVhZG9ubHkgc3RhdHVzQ29kZTogbnVtYmVyO1xuICBwdWJsaWMgcmVhZG9ubHkgY29kZTogc3RyaW5nO1xuXG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2U6IHN0cmluZywgc3RhdHVzQ29kZTogbnVtYmVyID0gNTAwLCBjb2RlOiBzdHJpbmcgPSAnVU5LTk9XTl9FUlJPUicpIHtcbiAgICBzdXBlcihtZXNzYWdlKTtcbiAgICB0aGlzLm5hbWUgPSAnV2lpUzNFcnJvcic7XG4gICAgdGhpcy5zdGF0dXNDb2RlID0gc3RhdHVzQ29kZTtcbiAgICB0aGlzLmNvZGUgPSBjb2RlO1xuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBdXRoZW50aWNhdGlvbkVycm9yIGV4dGVuZHMgV2lpUzNFcnJvciB7XG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2U6IHN0cmluZyA9ICdJbnZhbGlkIG9yIG1pc3NpbmcgQVBJIGtleScpIHtcbiAgICBzdXBlcihtZXNzYWdlLCA0MDEsICdBVVRIRU5USUNBVElPTl9FUlJPUicpO1xuICAgIHRoaXMubmFtZSA9ICdBdXRoZW50aWNhdGlvbkVycm9yJztcbiAgfVxufVxuXG5leHBvcnQgY2xhc3MgVmFsaWRhdGlvbkVycm9yIGV4dGVuZHMgV2lpUzNFcnJvciB7XG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2U6IHN0cmluZykge1xuICAgIHN1cGVyKG1lc3NhZ2UsIDQwMCwgJ1ZBTElEQVRJT05fRVJST1InKTtcbiAgICB0aGlzLm5hbWUgPSAnVmFsaWRhdGlvbkVycm9yJztcbiAgfVxufVxuXG5leHBvcnQgY2xhc3MgTm90Rm91bmRFcnJvciBleHRlbmRzIFdpaVMzRXJyb3Ige1xuICBjb25zdHJ1Y3RvcihtZXNzYWdlOiBzdHJpbmcgPSAnRmlsZSBub3QgZm91bmQnKSB7XG4gICAgc3VwZXIobWVzc2FnZSwgNDA0LCAnTk9UX0ZPVU5EJyk7XG4gICAgdGhpcy5uYW1lID0gJ05vdEZvdW5kRXJyb3InO1xuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBRdW90YUV4Y2VlZGVkRXJyb3IgZXh0ZW5kcyBXaWlTM0Vycm9yIHtcbiAgY29uc3RydWN0b3IobWVzc2FnZTogc3RyaW5nID0gJ1N0b3JhZ2UgcXVvdGEgZXhjZWVkZWQnKSB7XG4gICAgc3VwZXIobWVzc2FnZSwgNDAwLCAnUVVPVEFfRVhDRUVERUQnKTtcbiAgICB0aGlzLm5hbWUgPSAnUXVvdGFFeGNlZWRlZEVycm9yJztcbiAgfVxufVxuXG5leHBvcnQgY2xhc3MgTmV0d29ya0Vycm9yIGV4dGVuZHMgV2lpUzNFcnJvciB7XG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2U6IHN0cmluZyA9ICdOZXR3b3JrIGVycm9yIG9jY3VycmVkJykge1xuICAgIHN1cGVyKG1lc3NhZ2UsIDAsICdORVRXT1JLX0VSUk9SJyk7XG4gICAgdGhpcy5uYW1lID0gJ05ldHdvcmtFcnJvcic7XG4gIH1cbn1cbiJdfQ==
@@ -0,0 +1,3 @@
1
+ export { WiiS3Client } from './client';
2
+ export { WiiS3Config, UploadOptions, UploadedFile, FileInfo, DownloadUrlResponse, } from './types';
3
+ export { WiiS3Error, AuthenticationError, ValidationError, NotFoundError, QuotaExceededError, NetworkError, } from './errors';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkError = exports.QuotaExceededError = exports.NotFoundError = exports.ValidationError = exports.AuthenticationError = exports.WiiS3Error = exports.WiiS3Client = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "WiiS3Client", { enumerable: true, get: function () { return client_1.WiiS3Client; } });
6
+ var errors_1 = require("./errors");
7
+ Object.defineProperty(exports, "WiiS3Error", { enumerable: true, get: function () { return errors_1.WiiS3Error; } });
8
+ Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_1.AuthenticationError; } });
9
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_1.ValidationError; } });
10
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_1.NotFoundError; } });
11
+ Object.defineProperty(exports, "QuotaExceededError", { enumerable: true, get: function () { return errors_1.QuotaExceededError; } });
12
+ Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return errors_1.NetworkError; } });
13
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsbUNBQXVDO0FBQTlCLHFHQUFBLFdBQVcsT0FBQTtBQVFwQixtQ0FPa0I7QUFOaEIsb0dBQUEsVUFBVSxPQUFBO0FBQ1YsNkdBQUEsbUJBQW1CLE9BQUE7QUFDbkIseUdBQUEsZUFBZSxPQUFBO0FBQ2YsdUdBQUEsYUFBYSxPQUFBO0FBQ2IsNEdBQUEsa0JBQWtCLE9BQUE7QUFDbEIsc0dBQUEsWUFBWSxPQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHsgV2lpUzNDbGllbnQgfSBmcm9tICcuL2NsaWVudCc7XG5leHBvcnQge1xuICBXaWlTM0NvbmZpZyxcbiAgVXBsb2FkT3B0aW9ucyxcbiAgVXBsb2FkZWRGaWxlLFxuICBGaWxlSW5mbyxcbiAgRG93bmxvYWRVcmxSZXNwb25zZSxcbn0gZnJvbSAnLi90eXBlcyc7XG5leHBvcnQge1xuICBXaWlTM0Vycm9yLFxuICBBdXRoZW50aWNhdGlvbkVycm9yLFxuICBWYWxpZGF0aW9uRXJyb3IsXG4gIE5vdEZvdW5kRXJyb3IsXG4gIFF1b3RhRXhjZWVkZWRFcnJvcixcbiAgTmV0d29ya0Vycm9yLFxufSBmcm9tICcuL2Vycm9ycyc7XG4iXX0=
@@ -0,0 +1,40 @@
1
+ export interface WiiS3Config {
2
+ endpoint: string;
3
+ apiKey: string;
4
+ timeout?: number;
5
+ }
6
+ export interface UploadOptions {
7
+ userId?: string;
8
+ metadata?: Record<string, any>;
9
+ }
10
+ export interface UploadedFile {
11
+ id: string;
12
+ originalName: string;
13
+ storedName: string;
14
+ mimeType: string;
15
+ size: number;
16
+ publicUrl: string;
17
+ uploadedAt: string;
18
+ }
19
+ export interface FileInfo {
20
+ id: string;
21
+ originalName: string;
22
+ storedName: string;
23
+ mimeType: string;
24
+ size: number;
25
+ publicUrl: string;
26
+ userId?: string;
27
+ metadata?: Record<string, any>;
28
+ uploadedAt: string;
29
+ }
30
+ export interface DownloadUrlResponse {
31
+ url: string;
32
+ expiresIn: number;
33
+ }
34
+ export interface ApiResponse<T> {
35
+ success: boolean;
36
+ error?: string;
37
+ file?: T;
38
+ url?: string;
39
+ expiresIn?: number;
40
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgV2lpUzNDb25maWcge1xuICBlbmRwb2ludDogc3RyaW5nO1xuICBhcGlLZXk6IHN0cmluZztcbiAgdGltZW91dD86IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBVcGxvYWRPcHRpb25zIHtcbiAgdXNlcklkPzogc3RyaW5nO1xuICBtZXRhZGF0YT86IFJlY29yZDxzdHJpbmcsIGFueT47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVXBsb2FkZWRGaWxlIHtcbiAgaWQ6IHN0cmluZztcbiAgb3JpZ2luYWxOYW1lOiBzdHJpbmc7XG4gIHN0b3JlZE5hbWU6IHN0cmluZztcbiAgbWltZVR5cGU6IHN0cmluZztcbiAgc2l6ZTogbnVtYmVyO1xuICBwdWJsaWNVcmw6IHN0cmluZztcbiAgdXBsb2FkZWRBdDogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIEZpbGVJbmZvIHtcbiAgaWQ6IHN0cmluZztcbiAgb3JpZ2luYWxOYW1lOiBzdHJpbmc7XG4gIHN0b3JlZE5hbWU6IHN0cmluZztcbiAgbWltZVR5cGU6IHN0cmluZztcbiAgc2l6ZTogbnVtYmVyO1xuICBwdWJsaWNVcmw6IHN0cmluZztcbiAgdXNlcklkPzogc3RyaW5nO1xuICBtZXRhZGF0YT86IFJlY29yZDxzdHJpbmcsIGFueT47XG4gIHVwbG9hZGVkQXQ6IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBEb3dubG9hZFVybFJlc3BvbnNlIHtcbiAgdXJsOiBzdHJpbmc7XG4gIGV4cGlyZXNJbjogbnVtYmVyO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIEFwaVJlc3BvbnNlPFQ+IHtcbiAgc3VjY2VzczogYm9vbGVhbjtcbiAgZXJyb3I/OiBzdHJpbmc7XG4gIGZpbGU/OiBUO1xuICB1cmw/OiBzdHJpbmc7XG4gIGV4cGlyZXNJbj86IG51bWJlcjtcbn1cbiJdfQ==
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@wiicode/s3-client",
3
+ "version": "1.0.0",
4
+ "description": "Official SDK client for WiiCode S3 Upload Service",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "wiicode",
16
+ "s3",
17
+ "upload",
18
+ "file",
19
+ "storage",
20
+ "minio"
21
+ ],
22
+ "author": "WiiCode",
23
+ "license": "MIT",
24
+ "devDependencies": {
25
+ "typescript": "^5.0.0",
26
+ "@types/node": "^20.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "form-data": "^4.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "form-data": {
33
+ "optional": true
34
+ }
35
+ }
36
+ }