@marlinjai/storage-brain-sdk 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 +190 -0
- package/dist/index.cjs +369 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +367 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +347 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowed MIME types for file uploads
|
|
3
|
+
*/
|
|
4
|
+
declare const ALLOWED_FILE_TYPES: {
|
|
5
|
+
readonly 'image/jpeg': {
|
|
6
|
+
readonly extension: "jpg";
|
|
7
|
+
readonly category: "image";
|
|
8
|
+
};
|
|
9
|
+
readonly 'image/png': {
|
|
10
|
+
readonly extension: "png";
|
|
11
|
+
readonly category: "image";
|
|
12
|
+
};
|
|
13
|
+
readonly 'image/webp': {
|
|
14
|
+
readonly extension: "webp";
|
|
15
|
+
readonly category: "image";
|
|
16
|
+
};
|
|
17
|
+
readonly 'image/gif': {
|
|
18
|
+
readonly extension: "gif";
|
|
19
|
+
readonly category: "image";
|
|
20
|
+
};
|
|
21
|
+
readonly 'image/avif': {
|
|
22
|
+
readonly extension: "avif";
|
|
23
|
+
readonly category: "image";
|
|
24
|
+
};
|
|
25
|
+
readonly 'application/pdf': {
|
|
26
|
+
readonly extension: "pdf";
|
|
27
|
+
readonly category: "document";
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
type AllowedMimeType = keyof typeof ALLOWED_FILE_TYPES;
|
|
31
|
+
declare const ALLOWED_MIME_TYPES: AllowedMimeType[];
|
|
32
|
+
declare const IMAGE_MIME_TYPES: ("image/jpeg" | "image/png" | "image/webp" | "image/gif" | "image/avif" | "application/pdf")[];
|
|
33
|
+
declare const DOCUMENT_MIME_TYPES: ("image/jpeg" | "image/png" | "image/webp" | "image/gif" | "image/avif" | "application/pdf")[];
|
|
34
|
+
/**
|
|
35
|
+
* Processing contexts
|
|
36
|
+
*/
|
|
37
|
+
declare const PROCESSING_CONTEXTS: readonly ["newsletter", "invoice", "framer-site", "default"];
|
|
38
|
+
type ProcessingContext = (typeof PROCESSING_CONTEXTS)[number];
|
|
39
|
+
/**
|
|
40
|
+
* File size limits
|
|
41
|
+
*/
|
|
42
|
+
declare const MAX_FILE_SIZE_BYTES: number;
|
|
43
|
+
/**
|
|
44
|
+
* Thumbnail sizes
|
|
45
|
+
*/
|
|
46
|
+
declare const THUMBNAIL_SIZES: {
|
|
47
|
+
readonly thumb: {
|
|
48
|
+
readonly width: 200;
|
|
49
|
+
readonly height: 200;
|
|
50
|
+
};
|
|
51
|
+
readonly medium: {
|
|
52
|
+
readonly width: 400;
|
|
53
|
+
readonly height: 400;
|
|
54
|
+
};
|
|
55
|
+
readonly large: {
|
|
56
|
+
readonly width: 800;
|
|
57
|
+
readonly height: 800;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
type ThumbnailSize = keyof typeof THUMBNAIL_SIZES;
|
|
61
|
+
/**
|
|
62
|
+
* Processing statuses
|
|
63
|
+
*/
|
|
64
|
+
declare const PROCESSING_STATUSES: readonly ["pending", "processing", "completed", "failed"];
|
|
65
|
+
type ProcessingStatus = (typeof PROCESSING_STATUSES)[number];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* File metadata (stored as JSON)
|
|
69
|
+
*/
|
|
70
|
+
interface FileMetadata {
|
|
71
|
+
thumbnailUrls?: ThumbnailUrls;
|
|
72
|
+
ocrData?: OcrResult;
|
|
73
|
+
imageInfo?: ImageInfo;
|
|
74
|
+
processingError?: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Thumbnail URLs for different sizes
|
|
79
|
+
*/
|
|
80
|
+
type ThumbnailUrls = {
|
|
81
|
+
[K in ThumbnailSize]?: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* OCR result from Google Cloud Vision
|
|
85
|
+
*/
|
|
86
|
+
interface OcrResult {
|
|
87
|
+
fullText: string;
|
|
88
|
+
confidence: number;
|
|
89
|
+
blocks: OcrBlock[];
|
|
90
|
+
}
|
|
91
|
+
interface OcrBlock {
|
|
92
|
+
text: string;
|
|
93
|
+
confidence: number;
|
|
94
|
+
boundingBox: BoundingBox;
|
|
95
|
+
}
|
|
96
|
+
interface BoundingBox {
|
|
97
|
+
x: number;
|
|
98
|
+
y: number;
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Image information extracted from EXIF
|
|
104
|
+
*/
|
|
105
|
+
interface ImageInfo {
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
format: string;
|
|
109
|
+
colorSpace?: string;
|
|
110
|
+
hasAlpha?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Configuration for Storage Brain client
|
|
114
|
+
*/
|
|
115
|
+
interface StorageBrainConfig {
|
|
116
|
+
/** API key for authentication (sk_live_... or sk_test_...) */
|
|
117
|
+
apiKey: string;
|
|
118
|
+
/** Base URL of the Storage Brain API (defaults to production) */
|
|
119
|
+
baseUrl?: string;
|
|
120
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
121
|
+
timeout?: number;
|
|
122
|
+
/** Number of retry attempts for failed requests (default: 3) */
|
|
123
|
+
maxRetries?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Options for uploading a file
|
|
127
|
+
*/
|
|
128
|
+
interface UploadOptions {
|
|
129
|
+
/** Processing context for the file */
|
|
130
|
+
context: ProcessingContext;
|
|
131
|
+
/** Optional tags for the file */
|
|
132
|
+
tags?: Record<string, string>;
|
|
133
|
+
/** Progress callback (0-100) */
|
|
134
|
+
onProgress?: (progress: number) => void;
|
|
135
|
+
/** Optional webhook URL to call after processing */
|
|
136
|
+
webhookUrl?: string;
|
|
137
|
+
/** Abort signal for cancellation */
|
|
138
|
+
signal?: AbortSignal;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* File information returned from the API
|
|
142
|
+
*/
|
|
143
|
+
interface FileInfo {
|
|
144
|
+
/** Unique file identifier */
|
|
145
|
+
id: string;
|
|
146
|
+
/** Public URL to access the file */
|
|
147
|
+
url: string;
|
|
148
|
+
/** Original file name */
|
|
149
|
+
originalName: string;
|
|
150
|
+
/** MIME type */
|
|
151
|
+
fileType: AllowedMimeType;
|
|
152
|
+
/** File size in bytes */
|
|
153
|
+
sizeBytes: number;
|
|
154
|
+
/** Processing context */
|
|
155
|
+
context: ProcessingContext;
|
|
156
|
+
/** User-defined tags */
|
|
157
|
+
tags: Record<string, string> | null;
|
|
158
|
+
/** Processing results and metadata */
|
|
159
|
+
metadata: FileMetadata | null;
|
|
160
|
+
/** Current processing status */
|
|
161
|
+
processingStatus: ProcessingStatus;
|
|
162
|
+
/** ISO 8601 timestamp */
|
|
163
|
+
createdAt: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Options for listing files
|
|
167
|
+
*/
|
|
168
|
+
interface ListFilesOptions {
|
|
169
|
+
/** Maximum number of files to return (1-100, default: 20) */
|
|
170
|
+
limit?: number;
|
|
171
|
+
/** Cursor for pagination */
|
|
172
|
+
cursor?: string;
|
|
173
|
+
/** Filter by processing context */
|
|
174
|
+
context?: ProcessingContext;
|
|
175
|
+
/** Filter by file type */
|
|
176
|
+
fileType?: AllowedMimeType;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Result of listing files
|
|
180
|
+
*/
|
|
181
|
+
interface ListFilesResult {
|
|
182
|
+
/** Array of file information */
|
|
183
|
+
files: FileInfo[];
|
|
184
|
+
/** Cursor for next page, null if no more pages */
|
|
185
|
+
nextCursor: string | null;
|
|
186
|
+
/** Total number of files matching the query */
|
|
187
|
+
total: number;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Quota usage information
|
|
191
|
+
*/
|
|
192
|
+
interface QuotaInfo {
|
|
193
|
+
/** Total quota in bytes */
|
|
194
|
+
quotaBytes: number;
|
|
195
|
+
/** Used storage in bytes */
|
|
196
|
+
usedBytes: number;
|
|
197
|
+
/** Available storage in bytes */
|
|
198
|
+
availableBytes: number;
|
|
199
|
+
/** Usage percentage (0-100) */
|
|
200
|
+
usagePercent: number;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Tenant information
|
|
204
|
+
*/
|
|
205
|
+
interface TenantInfo {
|
|
206
|
+
/** Tenant ID */
|
|
207
|
+
id: string;
|
|
208
|
+
/** Tenant name */
|
|
209
|
+
name: string;
|
|
210
|
+
/** Allowed file types for this tenant */
|
|
211
|
+
allowedFileTypes: AllowedMimeType[] | null;
|
|
212
|
+
/** ISO 8601 timestamp */
|
|
213
|
+
createdAt: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Upload handshake response
|
|
217
|
+
*/
|
|
218
|
+
interface UploadHandshake {
|
|
219
|
+
/** File ID assigned to this upload */
|
|
220
|
+
fileId: string;
|
|
221
|
+
/** Presigned URL for uploading */
|
|
222
|
+
presignedUrl: string;
|
|
223
|
+
/** Expiration timestamp (ISO 8601) */
|
|
224
|
+
expiresAt: string;
|
|
225
|
+
/** Upload constraints */
|
|
226
|
+
uploadMetadata: {
|
|
227
|
+
maxSizeBytes: number;
|
|
228
|
+
allowedTypes: AllowedMimeType[];
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Storage Brain SDK Client
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const storage = new StorageBrain({
|
|
238
|
+
* apiKey: 'sk_live_...',
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* // Upload a file
|
|
242
|
+
* const file = await storage.upload(fileBlob, {
|
|
243
|
+
* context: 'newsletter',
|
|
244
|
+
* onProgress: (p) => console.log(`${p}%`),
|
|
245
|
+
* });
|
|
246
|
+
*
|
|
247
|
+
* console.log(file.url);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
declare class StorageBrain {
|
|
251
|
+
private readonly apiKey;
|
|
252
|
+
private readonly baseUrl;
|
|
253
|
+
private readonly timeout;
|
|
254
|
+
private readonly maxRetries;
|
|
255
|
+
constructor(config: StorageBrainConfig);
|
|
256
|
+
/**
|
|
257
|
+
* Upload a file
|
|
258
|
+
*/
|
|
259
|
+
upload(file: File | Blob, options: UploadOptions): Promise<FileInfo>;
|
|
260
|
+
/**
|
|
261
|
+
* Request an upload handshake
|
|
262
|
+
*/
|
|
263
|
+
private requestUpload;
|
|
264
|
+
/**
|
|
265
|
+
* Upload file content to presigned URL
|
|
266
|
+
*/
|
|
267
|
+
private uploadToPresignedUrl;
|
|
268
|
+
/**
|
|
269
|
+
* Wait for file processing to complete
|
|
270
|
+
*/
|
|
271
|
+
private waitForProcessing;
|
|
272
|
+
/**
|
|
273
|
+
* Get a file by ID
|
|
274
|
+
*/
|
|
275
|
+
getFile(fileId: string): Promise<FileInfo>;
|
|
276
|
+
/**
|
|
277
|
+
* List files
|
|
278
|
+
*/
|
|
279
|
+
listFiles(options?: ListFilesOptions): Promise<ListFilesResult>;
|
|
280
|
+
/**
|
|
281
|
+
* Delete a file
|
|
282
|
+
*/
|
|
283
|
+
deleteFile(fileId: string): Promise<void>;
|
|
284
|
+
/**
|
|
285
|
+
* Get quota usage
|
|
286
|
+
*/
|
|
287
|
+
getQuota(): Promise<QuotaInfo>;
|
|
288
|
+
/**
|
|
289
|
+
* Get tenant information
|
|
290
|
+
*/
|
|
291
|
+
getTenantInfo(): Promise<TenantInfo>;
|
|
292
|
+
/**
|
|
293
|
+
* Make an authenticated API request with retry logic
|
|
294
|
+
*/
|
|
295
|
+
private request;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Base error class for Storage Brain SDK
|
|
300
|
+
*/
|
|
301
|
+
declare class StorageBrainError extends Error {
|
|
302
|
+
code: string;
|
|
303
|
+
statusCode?: number | undefined;
|
|
304
|
+
details?: Record<string, unknown> | undefined;
|
|
305
|
+
constructor(message: string, code: string, statusCode?: number | undefined, details?: Record<string, unknown> | undefined);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Authentication error - invalid or missing API key
|
|
309
|
+
*/
|
|
310
|
+
declare class AuthenticationError extends StorageBrainError {
|
|
311
|
+
constructor(message?: string);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Quota exceeded error
|
|
315
|
+
*/
|
|
316
|
+
declare class QuotaExceededError extends StorageBrainError {
|
|
317
|
+
quotaBytes?: number | undefined;
|
|
318
|
+
usedBytes?: number | undefined;
|
|
319
|
+
constructor(message?: string, quotaBytes?: number | undefined, usedBytes?: number | undefined);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Invalid file type error
|
|
323
|
+
*/
|
|
324
|
+
declare class InvalidFileTypeError extends StorageBrainError {
|
|
325
|
+
constructor(fileType: string, allowedTypes?: string[]);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* File too large error
|
|
329
|
+
*/
|
|
330
|
+
declare class FileTooLargeError extends StorageBrainError {
|
|
331
|
+
constructor(fileSize: number, maxSize: number);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* File not found error
|
|
335
|
+
*/
|
|
336
|
+
declare class FileNotFoundError extends StorageBrainError {
|
|
337
|
+
constructor(fileId: string);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Network error - connection issues
|
|
341
|
+
*/
|
|
342
|
+
declare class NetworkError extends StorageBrainError {
|
|
343
|
+
originalError?: Error | undefined;
|
|
344
|
+
constructor(message?: string, originalError?: Error | undefined);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Upload error - file upload failed
|
|
348
|
+
*/
|
|
349
|
+
declare class UploadError extends StorageBrainError {
|
|
350
|
+
originalError?: Error | undefined;
|
|
351
|
+
constructor(message: string, originalError?: Error | undefined);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Validation error - request validation failed
|
|
355
|
+
*/
|
|
356
|
+
declare class ValidationError extends StorageBrainError {
|
|
357
|
+
errors?: Array<{
|
|
358
|
+
path: string;
|
|
359
|
+
message: string;
|
|
360
|
+
}> | undefined;
|
|
361
|
+
constructor(message: string, errors?: Array<{
|
|
362
|
+
path: string;
|
|
363
|
+
message: string;
|
|
364
|
+
}> | undefined);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export { ALLOWED_FILE_TYPES, ALLOWED_MIME_TYPES, type AllowedMimeType, AuthenticationError, type BoundingBox, DOCUMENT_MIME_TYPES, type FileInfo, type FileMetadata, FileNotFoundError, FileTooLargeError, IMAGE_MIME_TYPES, type ImageInfo, InvalidFileTypeError, type ListFilesOptions, type ListFilesResult, MAX_FILE_SIZE_BYTES, NetworkError, type OcrBlock, type OcrResult, PROCESSING_CONTEXTS, PROCESSING_STATUSES, type ProcessingContext, type ProcessingStatus, QuotaExceededError, type QuotaInfo, StorageBrain, type StorageBrainConfig, StorageBrainError, THUMBNAIL_SIZES, type TenantInfo, type ThumbnailSize, type ThumbnailUrls, UploadError, type UploadHandshake, type UploadOptions, ValidationError, StorageBrain as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var ALLOWED_FILE_TYPES = {
|
|
3
|
+
// Images
|
|
4
|
+
"image/jpeg": { extension: "jpg", category: "image" },
|
|
5
|
+
"image/png": { extension: "png", category: "image" },
|
|
6
|
+
"image/webp": { extension: "webp", category: "image" },
|
|
7
|
+
"image/gif": { extension: "gif", category: "image" },
|
|
8
|
+
"image/avif": { extension: "avif", category: "image" },
|
|
9
|
+
// Documents
|
|
10
|
+
"application/pdf": { extension: "pdf", category: "document" }
|
|
11
|
+
};
|
|
12
|
+
var ALLOWED_MIME_TYPES = Object.keys(ALLOWED_FILE_TYPES);
|
|
13
|
+
var IMAGE_MIME_TYPES = ALLOWED_MIME_TYPES.filter(
|
|
14
|
+
(type) => ALLOWED_FILE_TYPES[type].category === "image"
|
|
15
|
+
);
|
|
16
|
+
var DOCUMENT_MIME_TYPES = ALLOWED_MIME_TYPES.filter(
|
|
17
|
+
(type) => ALLOWED_FILE_TYPES[type].category === "document"
|
|
18
|
+
);
|
|
19
|
+
var PROCESSING_CONTEXTS = ["newsletter", "invoice", "framer-site", "default"];
|
|
20
|
+
var MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
21
|
+
var THUMBNAIL_SIZES = {
|
|
22
|
+
thumb: { width: 200, height: 200 },
|
|
23
|
+
medium: { width: 400, height: 400 },
|
|
24
|
+
large: { width: 800, height: 800 }
|
|
25
|
+
};
|
|
26
|
+
var PROCESSING_STATUSES = ["pending", "processing", "completed", "failed"];
|
|
27
|
+
var RETRY_CONFIG = {
|
|
28
|
+
initialDelayMs: 1e3,
|
|
29
|
+
maxDelayMs: 1e4,
|
|
30
|
+
backoffMultiplier: 2
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var StorageBrainError = class extends Error {
|
|
35
|
+
constructor(message, code, statusCode, details) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.statusCode = statusCode;
|
|
39
|
+
this.details = details;
|
|
40
|
+
this.name = "StorageBrainError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var AuthenticationError = class extends StorageBrainError {
|
|
44
|
+
constructor(message = "Authentication failed") {
|
|
45
|
+
super(message, "AUTHENTICATION_ERROR", 401);
|
|
46
|
+
this.name = "AuthenticationError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var QuotaExceededError = class extends StorageBrainError {
|
|
50
|
+
constructor(message = "Storage quota exceeded", quotaBytes, usedBytes) {
|
|
51
|
+
super(message, "QUOTA_EXCEEDED", 403, { quotaBytes, usedBytes });
|
|
52
|
+
this.quotaBytes = quotaBytes;
|
|
53
|
+
this.usedBytes = usedBytes;
|
|
54
|
+
this.name = "QuotaExceededError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var InvalidFileTypeError = class extends StorageBrainError {
|
|
58
|
+
constructor(fileType, allowedTypes) {
|
|
59
|
+
super(
|
|
60
|
+
`File type '${fileType}' is not allowed`,
|
|
61
|
+
"INVALID_FILE_TYPE",
|
|
62
|
+
400,
|
|
63
|
+
{ fileType, allowedTypes }
|
|
64
|
+
);
|
|
65
|
+
this.name = "InvalidFileTypeError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var FileTooLargeError = class extends StorageBrainError {
|
|
69
|
+
constructor(fileSize, maxSize) {
|
|
70
|
+
super(
|
|
71
|
+
`File size ${fileSize} bytes exceeds maximum of ${maxSize} bytes`,
|
|
72
|
+
"FILE_TOO_LARGE",
|
|
73
|
+
400,
|
|
74
|
+
{ fileSize, maxSize }
|
|
75
|
+
);
|
|
76
|
+
this.name = "FileTooLargeError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var FileNotFoundError = class extends StorageBrainError {
|
|
80
|
+
constructor(fileId) {
|
|
81
|
+
super(`File not found: ${fileId}`, "FILE_NOT_FOUND", 404, { fileId });
|
|
82
|
+
this.name = "FileNotFoundError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var NetworkError = class extends StorageBrainError {
|
|
86
|
+
constructor(message = "Network error occurred", originalError) {
|
|
87
|
+
super(message, "NETWORK_ERROR", void 0, { originalError: originalError?.message });
|
|
88
|
+
this.originalError = originalError;
|
|
89
|
+
this.name = "NetworkError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var UploadError = class extends StorageBrainError {
|
|
93
|
+
constructor(message, originalError) {
|
|
94
|
+
super(message, "UPLOAD_ERROR", void 0, { originalError: originalError?.message });
|
|
95
|
+
this.originalError = originalError;
|
|
96
|
+
this.name = "UploadError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var ValidationError = class extends StorageBrainError {
|
|
100
|
+
constructor(message, errors) {
|
|
101
|
+
super(message, "VALIDATION_ERROR", 400, { errors });
|
|
102
|
+
this.errors = errors;
|
|
103
|
+
this.name = "ValidationError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
function parseApiError(statusCode, response) {
|
|
107
|
+
const { code, message, details } = response.error ?? {};
|
|
108
|
+
switch (code) {
|
|
109
|
+
case "UNAUTHORIZED":
|
|
110
|
+
return new AuthenticationError(message);
|
|
111
|
+
case "QUOTA_EXCEEDED":
|
|
112
|
+
return new QuotaExceededError(
|
|
113
|
+
message,
|
|
114
|
+
details?.quotaBytes,
|
|
115
|
+
details?.usedBytes
|
|
116
|
+
);
|
|
117
|
+
case "INVALID_FILE_TYPE":
|
|
118
|
+
return new InvalidFileTypeError(
|
|
119
|
+
details?.fileType,
|
|
120
|
+
details?.allowedTypes
|
|
121
|
+
);
|
|
122
|
+
case "FILE_TOO_LARGE":
|
|
123
|
+
return new FileTooLargeError(
|
|
124
|
+
details?.fileSize,
|
|
125
|
+
details?.maxSize
|
|
126
|
+
);
|
|
127
|
+
case "FILE_NOT_FOUND":
|
|
128
|
+
case "NOT_FOUND":
|
|
129
|
+
return new FileNotFoundError(details?.fileId ?? "unknown");
|
|
130
|
+
case "VALIDATION_ERROR":
|
|
131
|
+
return new ValidationError(
|
|
132
|
+
message ?? "Validation failed",
|
|
133
|
+
details?.errors
|
|
134
|
+
);
|
|
135
|
+
default:
|
|
136
|
+
return new StorageBrainError(
|
|
137
|
+
message ?? "An error occurred",
|
|
138
|
+
code ?? "UNKNOWN_ERROR",
|
|
139
|
+
statusCode,
|
|
140
|
+
details
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/client.ts
|
|
146
|
+
var DEFAULT_BASE_URL = "https://storage-brain-api.marlin-pohl.workers.dev";
|
|
147
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
148
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
149
|
+
var StorageBrain = class {
|
|
150
|
+
apiKey;
|
|
151
|
+
baseUrl;
|
|
152
|
+
timeout;
|
|
153
|
+
maxRetries;
|
|
154
|
+
constructor(config) {
|
|
155
|
+
if (!config.apiKey) {
|
|
156
|
+
throw new StorageBrainError("API key is required", "CONFIGURATION_ERROR");
|
|
157
|
+
}
|
|
158
|
+
this.apiKey = config.apiKey;
|
|
159
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
160
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
161
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Upload a file
|
|
165
|
+
*/
|
|
166
|
+
async upload(file, options) {
|
|
167
|
+
const { context, tags, onProgress, webhookUrl, signal } = options;
|
|
168
|
+
const fileName = file instanceof File ? file.name : "file";
|
|
169
|
+
const fileType = file.type;
|
|
170
|
+
const fileSize = file.size;
|
|
171
|
+
if (!ALLOWED_MIME_TYPES.includes(fileType)) {
|
|
172
|
+
throw new InvalidFileTypeError(fileType, [...ALLOWED_MIME_TYPES]);
|
|
173
|
+
}
|
|
174
|
+
const handshake = await this.requestUpload({
|
|
175
|
+
fileType,
|
|
176
|
+
fileName,
|
|
177
|
+
fileSizeBytes: fileSize,
|
|
178
|
+
context,
|
|
179
|
+
tags,
|
|
180
|
+
webhookUrl
|
|
181
|
+
});
|
|
182
|
+
onProgress?.(10);
|
|
183
|
+
await this.uploadToPresignedUrl(handshake.presignedUrl, file, fileType, (progress) => {
|
|
184
|
+
onProgress?.(10 + Math.round(progress * 0.8));
|
|
185
|
+
}, signal);
|
|
186
|
+
onProgress?.(90);
|
|
187
|
+
const fileInfo = await this.waitForProcessing(handshake.fileId, signal);
|
|
188
|
+
onProgress?.(100);
|
|
189
|
+
return fileInfo;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Request an upload handshake
|
|
193
|
+
*/
|
|
194
|
+
async requestUpload(params) {
|
|
195
|
+
return this.request("POST", "/api/v1/upload/request", params);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Upload file content to presigned URL
|
|
199
|
+
*/
|
|
200
|
+
async uploadToPresignedUrl(presignedUrl, file, contentType, onProgress, signal) {
|
|
201
|
+
const uploadUrl = presignedUrl.startsWith("/") ? `${this.baseUrl}${presignedUrl}` : presignedUrl;
|
|
202
|
+
try {
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
const xhr = new XMLHttpRequest();
|
|
205
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
206
|
+
if (event.lengthComputable && onProgress) {
|
|
207
|
+
onProgress(Math.round(event.loaded / event.total * 100));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
xhr.addEventListener("load", () => {
|
|
211
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
212
|
+
resolve();
|
|
213
|
+
} else {
|
|
214
|
+
reject(new UploadError(`Upload failed with status ${xhr.status}`));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
xhr.addEventListener("error", () => {
|
|
218
|
+
reject(new NetworkError("Network error during upload"));
|
|
219
|
+
});
|
|
220
|
+
xhr.addEventListener("abort", () => {
|
|
221
|
+
reject(new UploadError("Upload was cancelled"));
|
|
222
|
+
});
|
|
223
|
+
if (signal) {
|
|
224
|
+
signal.addEventListener("abort", () => {
|
|
225
|
+
xhr.abort();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
xhr.open("PUT", uploadUrl);
|
|
229
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
230
|
+
if (presignedUrl.startsWith("/")) {
|
|
231
|
+
xhr.setRequestHeader("Authorization", `Bearer ${this.apiKey}`);
|
|
232
|
+
}
|
|
233
|
+
xhr.send(file);
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof StorageBrainError) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
throw new UploadError(
|
|
240
|
+
"Failed to upload file",
|
|
241
|
+
error instanceof Error ? error : void 0
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Wait for file processing to complete
|
|
247
|
+
*/
|
|
248
|
+
async waitForProcessing(fileId, signal, maxWaitMs = 6e4, pollIntervalMs = 1e3) {
|
|
249
|
+
const startTime = Date.now();
|
|
250
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
251
|
+
if (signal?.aborted) {
|
|
252
|
+
throw new UploadError("Operation was cancelled");
|
|
253
|
+
}
|
|
254
|
+
const file = await this.getFile(fileId);
|
|
255
|
+
if (file.processingStatus === "completed" || file.processingStatus === "failed") {
|
|
256
|
+
return file;
|
|
257
|
+
}
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
259
|
+
}
|
|
260
|
+
return this.getFile(fileId);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get a file by ID
|
|
264
|
+
*/
|
|
265
|
+
async getFile(fileId) {
|
|
266
|
+
return this.request("GET", `/api/v1/files/${fileId}`);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* List files
|
|
270
|
+
*/
|
|
271
|
+
async listFiles(options) {
|
|
272
|
+
const params = new URLSearchParams();
|
|
273
|
+
if (options?.limit) params.set("limit", options.limit.toString());
|
|
274
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
275
|
+
if (options?.context) params.set("context", options.context);
|
|
276
|
+
if (options?.fileType) params.set("fileType", options.fileType);
|
|
277
|
+
const query = params.toString();
|
|
278
|
+
const path = query ? `/api/v1/files?${query}` : "/api/v1/files";
|
|
279
|
+
return this.request("GET", path);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Delete a file
|
|
283
|
+
*/
|
|
284
|
+
async deleteFile(fileId) {
|
|
285
|
+
await this.request("DELETE", `/api/v1/files/${fileId}`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get quota usage
|
|
289
|
+
*/
|
|
290
|
+
async getQuota() {
|
|
291
|
+
return this.request("GET", "/api/v1/tenant/quota");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get tenant information
|
|
295
|
+
*/
|
|
296
|
+
async getTenantInfo() {
|
|
297
|
+
return this.request("GET", "/api/v1/tenant/info");
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Make an authenticated API request with retry logic
|
|
301
|
+
*/
|
|
302
|
+
async request(method, path, body) {
|
|
303
|
+
const url = `${this.baseUrl}${path}`;
|
|
304
|
+
let lastError;
|
|
305
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
306
|
+
try {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
309
|
+
const response = await fetch(url, {
|
|
310
|
+
method,
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "application/json",
|
|
313
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
314
|
+
},
|
|
315
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
316
|
+
signal: controller.signal
|
|
317
|
+
});
|
|
318
|
+
clearTimeout(timeoutId);
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
321
|
+
throw parseApiError(response.status, errorBody);
|
|
322
|
+
}
|
|
323
|
+
return await response.json();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
326
|
+
if (error instanceof StorageBrainError && error.statusCode && error.statusCode < 500) {
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
if (attempt < this.maxRetries - 1) {
|
|
330
|
+
const delay = Math.min(
|
|
331
|
+
RETRY_CONFIG.initialDelayMs * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt),
|
|
332
|
+
RETRY_CONFIG.maxDelayMs
|
|
333
|
+
);
|
|
334
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw new NetworkError(
|
|
339
|
+
`Request failed after ${this.maxRetries} attempts`,
|
|
340
|
+
lastError
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export { ALLOWED_FILE_TYPES, ALLOWED_MIME_TYPES, AuthenticationError, DOCUMENT_MIME_TYPES, FileNotFoundError, FileTooLargeError, IMAGE_MIME_TYPES, InvalidFileTypeError, MAX_FILE_SIZE_BYTES, NetworkError, PROCESSING_CONTEXTS, PROCESSING_STATUSES, QuotaExceededError, StorageBrain, StorageBrainError, THUMBNAIL_SIZES, UploadError, ValidationError, StorageBrain as default };
|
|
346
|
+
//# sourceMappingURL=index.js.map
|
|
347
|
+
//# sourceMappingURL=index.js.map
|