@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 marlinjai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # @marlinjai/storage-brain-sdk
2
+
3
+ TypeScript SDK for **Storage Brain** - an edge-native file storage service built on Cloudflare Workers, R2, and D1.
4
+
5
+ ## Features
6
+
7
+ - **Simple API** - Upload, download, list, and delete files with ease
8
+ - **Progress tracking** - Real-time upload progress callbacks
9
+ - **Type-safe** - Full TypeScript support with comprehensive types
10
+ - **Automatic retries** - Built-in retry logic with exponential backoff
11
+ - **Context-aware processing** - OCR, thumbnails, and more based on file context
12
+ - **Zero dependencies** - Uses only native browser/Node.js APIs
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @marlinjai/storage-brain-sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { StorageBrain } from '@marlinjai/storage-brain-sdk';
24
+
25
+ // Initialize the client
26
+ const storage = new StorageBrain({
27
+ apiKey: 'sk_live_your_api_key_here',
28
+ });
29
+
30
+ // Upload a file
31
+ const file = await storage.upload(fileBlob, {
32
+ context: 'invoice', // Triggers OCR processing
33
+ onProgress: (progress) => console.log(`${progress}%`),
34
+ });
35
+
36
+ console.log('File uploaded:', file.url);
37
+ console.log('OCR data:', file.metadata?.ocrData);
38
+ ```
39
+
40
+ ## API Reference
41
+
42
+ ### Constructor
43
+
44
+ ```typescript
45
+ const storage = new StorageBrain({
46
+ apiKey: string; // Required: Your API key
47
+ baseUrl?: string; // Optional: API base URL
48
+ timeout?: number; // Optional: Request timeout in ms (default: 30000)
49
+ maxRetries?: number; // Optional: Max retry attempts (default: 3)
50
+ });
51
+ ```
52
+
53
+ ### Methods
54
+
55
+ #### `upload(file, options)`
56
+
57
+ Upload a file to Storage Brain.
58
+
59
+ ```typescript
60
+ const result = await storage.upload(file, {
61
+ context: 'invoice', // Processing context
62
+ tags: { orderId: '123' }, // Optional metadata tags
63
+ onProgress: (p) => {}, // Progress callback (0-100)
64
+ webhookUrl: 'https://...', // Optional webhook for notifications
65
+ signal: abortController.signal // Optional abort signal
66
+ });
67
+ ```
68
+
69
+ **Processing Contexts:**
70
+ - `invoice` - OCR text extraction via Google Cloud Vision
71
+ - `newsletter` - Image validation and EXIF extraction
72
+ - `framer-site` - Thumbnail generation (200, 400, 800px)
73
+ - `default` - Basic validation and storage
74
+
75
+ #### `getFile(fileId)`
76
+
77
+ Get file information by ID.
78
+
79
+ ```typescript
80
+ const file = await storage.getFile('file-uuid');
81
+ ```
82
+
83
+ #### `listFiles(options?)`
84
+
85
+ List files with optional filtering and pagination.
86
+
87
+ ```typescript
88
+ const { files, nextCursor, total } = await storage.listFiles({
89
+ limit: 20, // Max results (default: 20)
90
+ cursor: 'abc...', // Pagination cursor
91
+ context: 'invoice', // Filter by context
92
+ fileType: 'image/png', // Filter by MIME type
93
+ });
94
+ ```
95
+
96
+ #### `deleteFile(fileId)`
97
+
98
+ Soft delete a file.
99
+
100
+ ```typescript
101
+ await storage.deleteFile('file-uuid');
102
+ ```
103
+
104
+ #### `getQuota()`
105
+
106
+ Get storage quota information.
107
+
108
+ ```typescript
109
+ const quota = await storage.getQuota();
110
+ console.log(`Used: ${quota.usedBytes} / ${quota.quotaBytes} bytes`);
111
+ console.log(`Usage: ${quota.usagePercent}%`);
112
+ ```
113
+
114
+ #### `getTenantInfo()`
115
+
116
+ Get tenant information.
117
+
118
+ ```typescript
119
+ const tenant = await storage.getTenantInfo();
120
+ console.log(`Tenant: ${tenant.name}`);
121
+ ```
122
+
123
+ ## Supported File Types
124
+
125
+ - **Images:** JPEG, PNG, WebP, GIF, AVIF
126
+ - **Documents:** PDF
127
+
128
+ ## Error Handling
129
+
130
+ The SDK throws typed errors for different scenarios:
131
+
132
+ ```typescript
133
+ import {
134
+ StorageBrainError,
135
+ AuthenticationError,
136
+ QuotaExceededError,
137
+ InvalidFileTypeError,
138
+ FileTooLargeError,
139
+ FileNotFoundError,
140
+ NetworkError,
141
+ UploadError,
142
+ } from '@marlinjai/storage-brain-sdk';
143
+
144
+ try {
145
+ await storage.upload(file, { context: 'invoice' });
146
+ } catch (error) {
147
+ if (error instanceof QuotaExceededError) {
148
+ console.error('Storage quota exceeded');
149
+ } else if (error instanceof InvalidFileTypeError) {
150
+ console.error('File type not allowed');
151
+ } else if (error instanceof AuthenticationError) {
152
+ console.error('Invalid API key');
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## TypeScript Types
158
+
159
+ All types are exported for your convenience:
160
+
161
+ ```typescript
162
+ import type {
163
+ StorageBrainConfig,
164
+ UploadOptions,
165
+ FileInfo,
166
+ ListFilesOptions,
167
+ ListFilesResult,
168
+ QuotaInfo,
169
+ TenantInfo,
170
+ FileMetadata,
171
+ OcrResult,
172
+ ProcessingContext,
173
+ AllowedMimeType,
174
+ } from '@marlinjai/storage-brain-sdk';
175
+ ```
176
+
177
+ ## Constants
178
+
179
+ ```typescript
180
+ import {
181
+ ALLOWED_MIME_TYPES, // ['image/jpeg', 'image/png', ...]
182
+ PROCESSING_CONTEXTS, // ['newsletter', 'invoice', 'framer-site', 'default']
183
+ MAX_FILE_SIZE_BYTES, // 100MB
184
+ THUMBNAIL_SIZES, // { thumb: 200, medium: 400, large: 800 }
185
+ } from '@marlinjai/storage-brain-sdk';
186
+ ```
187
+
188
+ ## License
189
+
190
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,369 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/constants.ts
6
+ var ALLOWED_FILE_TYPES = {
7
+ // Images
8
+ "image/jpeg": { extension: "jpg", category: "image" },
9
+ "image/png": { extension: "png", category: "image" },
10
+ "image/webp": { extension: "webp", category: "image" },
11
+ "image/gif": { extension: "gif", category: "image" },
12
+ "image/avif": { extension: "avif", category: "image" },
13
+ // Documents
14
+ "application/pdf": { extension: "pdf", category: "document" }
15
+ };
16
+ var ALLOWED_MIME_TYPES = Object.keys(ALLOWED_FILE_TYPES);
17
+ var IMAGE_MIME_TYPES = ALLOWED_MIME_TYPES.filter(
18
+ (type) => ALLOWED_FILE_TYPES[type].category === "image"
19
+ );
20
+ var DOCUMENT_MIME_TYPES = ALLOWED_MIME_TYPES.filter(
21
+ (type) => ALLOWED_FILE_TYPES[type].category === "document"
22
+ );
23
+ var PROCESSING_CONTEXTS = ["newsletter", "invoice", "framer-site", "default"];
24
+ var MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
25
+ var THUMBNAIL_SIZES = {
26
+ thumb: { width: 200, height: 200 },
27
+ medium: { width: 400, height: 400 },
28
+ large: { width: 800, height: 800 }
29
+ };
30
+ var PROCESSING_STATUSES = ["pending", "processing", "completed", "failed"];
31
+ var RETRY_CONFIG = {
32
+ initialDelayMs: 1e3,
33
+ maxDelayMs: 1e4,
34
+ backoffMultiplier: 2
35
+ };
36
+
37
+ // src/errors.ts
38
+ var StorageBrainError = class extends Error {
39
+ constructor(message, code, statusCode, details) {
40
+ super(message);
41
+ this.code = code;
42
+ this.statusCode = statusCode;
43
+ this.details = details;
44
+ this.name = "StorageBrainError";
45
+ }
46
+ };
47
+ var AuthenticationError = class extends StorageBrainError {
48
+ constructor(message = "Authentication failed") {
49
+ super(message, "AUTHENTICATION_ERROR", 401);
50
+ this.name = "AuthenticationError";
51
+ }
52
+ };
53
+ var QuotaExceededError = class extends StorageBrainError {
54
+ constructor(message = "Storage quota exceeded", quotaBytes, usedBytes) {
55
+ super(message, "QUOTA_EXCEEDED", 403, { quotaBytes, usedBytes });
56
+ this.quotaBytes = quotaBytes;
57
+ this.usedBytes = usedBytes;
58
+ this.name = "QuotaExceededError";
59
+ }
60
+ };
61
+ var InvalidFileTypeError = class extends StorageBrainError {
62
+ constructor(fileType, allowedTypes) {
63
+ super(
64
+ `File type '${fileType}' is not allowed`,
65
+ "INVALID_FILE_TYPE",
66
+ 400,
67
+ { fileType, allowedTypes }
68
+ );
69
+ this.name = "InvalidFileTypeError";
70
+ }
71
+ };
72
+ var FileTooLargeError = class extends StorageBrainError {
73
+ constructor(fileSize, maxSize) {
74
+ super(
75
+ `File size ${fileSize} bytes exceeds maximum of ${maxSize} bytes`,
76
+ "FILE_TOO_LARGE",
77
+ 400,
78
+ { fileSize, maxSize }
79
+ );
80
+ this.name = "FileTooLargeError";
81
+ }
82
+ };
83
+ var FileNotFoundError = class extends StorageBrainError {
84
+ constructor(fileId) {
85
+ super(`File not found: ${fileId}`, "FILE_NOT_FOUND", 404, { fileId });
86
+ this.name = "FileNotFoundError";
87
+ }
88
+ };
89
+ var NetworkError = class extends StorageBrainError {
90
+ constructor(message = "Network error occurred", originalError) {
91
+ super(message, "NETWORK_ERROR", void 0, { originalError: originalError?.message });
92
+ this.originalError = originalError;
93
+ this.name = "NetworkError";
94
+ }
95
+ };
96
+ var UploadError = class extends StorageBrainError {
97
+ constructor(message, originalError) {
98
+ super(message, "UPLOAD_ERROR", void 0, { originalError: originalError?.message });
99
+ this.originalError = originalError;
100
+ this.name = "UploadError";
101
+ }
102
+ };
103
+ var ValidationError = class extends StorageBrainError {
104
+ constructor(message, errors) {
105
+ super(message, "VALIDATION_ERROR", 400, { errors });
106
+ this.errors = errors;
107
+ this.name = "ValidationError";
108
+ }
109
+ };
110
+ function parseApiError(statusCode, response) {
111
+ const { code, message, details } = response.error ?? {};
112
+ switch (code) {
113
+ case "UNAUTHORIZED":
114
+ return new AuthenticationError(message);
115
+ case "QUOTA_EXCEEDED":
116
+ return new QuotaExceededError(
117
+ message,
118
+ details?.quotaBytes,
119
+ details?.usedBytes
120
+ );
121
+ case "INVALID_FILE_TYPE":
122
+ return new InvalidFileTypeError(
123
+ details?.fileType,
124
+ details?.allowedTypes
125
+ );
126
+ case "FILE_TOO_LARGE":
127
+ return new FileTooLargeError(
128
+ details?.fileSize,
129
+ details?.maxSize
130
+ );
131
+ case "FILE_NOT_FOUND":
132
+ case "NOT_FOUND":
133
+ return new FileNotFoundError(details?.fileId ?? "unknown");
134
+ case "VALIDATION_ERROR":
135
+ return new ValidationError(
136
+ message ?? "Validation failed",
137
+ details?.errors
138
+ );
139
+ default:
140
+ return new StorageBrainError(
141
+ message ?? "An error occurred",
142
+ code ?? "UNKNOWN_ERROR",
143
+ statusCode,
144
+ details
145
+ );
146
+ }
147
+ }
148
+
149
+ // src/client.ts
150
+ var DEFAULT_BASE_URL = "https://storage-brain-api.marlin-pohl.workers.dev";
151
+ var DEFAULT_TIMEOUT = 3e4;
152
+ var DEFAULT_MAX_RETRIES = 3;
153
+ var StorageBrain = class {
154
+ apiKey;
155
+ baseUrl;
156
+ timeout;
157
+ maxRetries;
158
+ constructor(config) {
159
+ if (!config.apiKey) {
160
+ throw new StorageBrainError("API key is required", "CONFIGURATION_ERROR");
161
+ }
162
+ this.apiKey = config.apiKey;
163
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
164
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
165
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
166
+ }
167
+ /**
168
+ * Upload a file
169
+ */
170
+ async upload(file, options) {
171
+ const { context, tags, onProgress, webhookUrl, signal } = options;
172
+ const fileName = file instanceof File ? file.name : "file";
173
+ const fileType = file.type;
174
+ const fileSize = file.size;
175
+ if (!ALLOWED_MIME_TYPES.includes(fileType)) {
176
+ throw new InvalidFileTypeError(fileType, [...ALLOWED_MIME_TYPES]);
177
+ }
178
+ const handshake = await this.requestUpload({
179
+ fileType,
180
+ fileName,
181
+ fileSizeBytes: fileSize,
182
+ context,
183
+ tags,
184
+ webhookUrl
185
+ });
186
+ onProgress?.(10);
187
+ await this.uploadToPresignedUrl(handshake.presignedUrl, file, fileType, (progress) => {
188
+ onProgress?.(10 + Math.round(progress * 0.8));
189
+ }, signal);
190
+ onProgress?.(90);
191
+ const fileInfo = await this.waitForProcessing(handshake.fileId, signal);
192
+ onProgress?.(100);
193
+ return fileInfo;
194
+ }
195
+ /**
196
+ * Request an upload handshake
197
+ */
198
+ async requestUpload(params) {
199
+ return this.request("POST", "/api/v1/upload/request", params);
200
+ }
201
+ /**
202
+ * Upload file content to presigned URL
203
+ */
204
+ async uploadToPresignedUrl(presignedUrl, file, contentType, onProgress, signal) {
205
+ const uploadUrl = presignedUrl.startsWith("/") ? `${this.baseUrl}${presignedUrl}` : presignedUrl;
206
+ try {
207
+ await new Promise((resolve, reject) => {
208
+ const xhr = new XMLHttpRequest();
209
+ xhr.upload.addEventListener("progress", (event) => {
210
+ if (event.lengthComputable && onProgress) {
211
+ onProgress(Math.round(event.loaded / event.total * 100));
212
+ }
213
+ });
214
+ xhr.addEventListener("load", () => {
215
+ if (xhr.status >= 200 && xhr.status < 300) {
216
+ resolve();
217
+ } else {
218
+ reject(new UploadError(`Upload failed with status ${xhr.status}`));
219
+ }
220
+ });
221
+ xhr.addEventListener("error", () => {
222
+ reject(new NetworkError("Network error during upload"));
223
+ });
224
+ xhr.addEventListener("abort", () => {
225
+ reject(new UploadError("Upload was cancelled"));
226
+ });
227
+ if (signal) {
228
+ signal.addEventListener("abort", () => {
229
+ xhr.abort();
230
+ });
231
+ }
232
+ xhr.open("PUT", uploadUrl);
233
+ xhr.setRequestHeader("Content-Type", contentType);
234
+ if (presignedUrl.startsWith("/")) {
235
+ xhr.setRequestHeader("Authorization", `Bearer ${this.apiKey}`);
236
+ }
237
+ xhr.send(file);
238
+ });
239
+ } catch (error) {
240
+ if (error instanceof StorageBrainError) {
241
+ throw error;
242
+ }
243
+ throw new UploadError(
244
+ "Failed to upload file",
245
+ error instanceof Error ? error : void 0
246
+ );
247
+ }
248
+ }
249
+ /**
250
+ * Wait for file processing to complete
251
+ */
252
+ async waitForProcessing(fileId, signal, maxWaitMs = 6e4, pollIntervalMs = 1e3) {
253
+ const startTime = Date.now();
254
+ while (Date.now() - startTime < maxWaitMs) {
255
+ if (signal?.aborted) {
256
+ throw new UploadError("Operation was cancelled");
257
+ }
258
+ const file = await this.getFile(fileId);
259
+ if (file.processingStatus === "completed" || file.processingStatus === "failed") {
260
+ return file;
261
+ }
262
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
263
+ }
264
+ return this.getFile(fileId);
265
+ }
266
+ /**
267
+ * Get a file by ID
268
+ */
269
+ async getFile(fileId) {
270
+ return this.request("GET", `/api/v1/files/${fileId}`);
271
+ }
272
+ /**
273
+ * List files
274
+ */
275
+ async listFiles(options) {
276
+ const params = new URLSearchParams();
277
+ if (options?.limit) params.set("limit", options.limit.toString());
278
+ if (options?.cursor) params.set("cursor", options.cursor);
279
+ if (options?.context) params.set("context", options.context);
280
+ if (options?.fileType) params.set("fileType", options.fileType);
281
+ const query = params.toString();
282
+ const path = query ? `/api/v1/files?${query}` : "/api/v1/files";
283
+ return this.request("GET", path);
284
+ }
285
+ /**
286
+ * Delete a file
287
+ */
288
+ async deleteFile(fileId) {
289
+ await this.request("DELETE", `/api/v1/files/${fileId}`);
290
+ }
291
+ /**
292
+ * Get quota usage
293
+ */
294
+ async getQuota() {
295
+ return this.request("GET", "/api/v1/tenant/quota");
296
+ }
297
+ /**
298
+ * Get tenant information
299
+ */
300
+ async getTenantInfo() {
301
+ return this.request("GET", "/api/v1/tenant/info");
302
+ }
303
+ /**
304
+ * Make an authenticated API request with retry logic
305
+ */
306
+ async request(method, path, body) {
307
+ const url = `${this.baseUrl}${path}`;
308
+ let lastError;
309
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
310
+ try {
311
+ const controller = new AbortController();
312
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
313
+ const response = await fetch(url, {
314
+ method,
315
+ headers: {
316
+ "Content-Type": "application/json",
317
+ Authorization: `Bearer ${this.apiKey}`
318
+ },
319
+ body: body ? JSON.stringify(body) : void 0,
320
+ signal: controller.signal
321
+ });
322
+ clearTimeout(timeoutId);
323
+ if (!response.ok) {
324
+ const errorBody = await response.json().catch(() => ({}));
325
+ throw parseApiError(response.status, errorBody);
326
+ }
327
+ return await response.json();
328
+ } catch (error) {
329
+ lastError = error instanceof Error ? error : new Error(String(error));
330
+ if (error instanceof StorageBrainError && error.statusCode && error.statusCode < 500) {
331
+ throw error;
332
+ }
333
+ if (attempt < this.maxRetries - 1) {
334
+ const delay = Math.min(
335
+ RETRY_CONFIG.initialDelayMs * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt),
336
+ RETRY_CONFIG.maxDelayMs
337
+ );
338
+ await new Promise((resolve) => setTimeout(resolve, delay));
339
+ }
340
+ }
341
+ }
342
+ throw new NetworkError(
343
+ `Request failed after ${this.maxRetries} attempts`,
344
+ lastError
345
+ );
346
+ }
347
+ };
348
+
349
+ exports.ALLOWED_FILE_TYPES = ALLOWED_FILE_TYPES;
350
+ exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
351
+ exports.AuthenticationError = AuthenticationError;
352
+ exports.DOCUMENT_MIME_TYPES = DOCUMENT_MIME_TYPES;
353
+ exports.FileNotFoundError = FileNotFoundError;
354
+ exports.FileTooLargeError = FileTooLargeError;
355
+ exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
356
+ exports.InvalidFileTypeError = InvalidFileTypeError;
357
+ exports.MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_BYTES;
358
+ exports.NetworkError = NetworkError;
359
+ exports.PROCESSING_CONTEXTS = PROCESSING_CONTEXTS;
360
+ exports.PROCESSING_STATUSES = PROCESSING_STATUSES;
361
+ exports.QuotaExceededError = QuotaExceededError;
362
+ exports.StorageBrain = StorageBrain;
363
+ exports.StorageBrainError = StorageBrainError;
364
+ exports.THUMBNAIL_SIZES = THUMBNAIL_SIZES;
365
+ exports.UploadError = UploadError;
366
+ exports.ValidationError = ValidationError;
367
+ exports.default = StorageBrain;
368
+ //# sourceMappingURL=index.cjs.map
369
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Allowed MIME types for file uploads\n */\nexport const ALLOWED_FILE_TYPES = {\n // Images\n 'image/jpeg': { extension: 'jpg', category: 'image' },\n 'image/png': { extension: 'png', category: 'image' },\n 'image/webp': { extension: 'webp', category: 'image' },\n 'image/gif': { extension: 'gif', category: 'image' },\n 'image/avif': { extension: 'avif', category: 'image' },\n // Documents\n 'application/pdf': { extension: 'pdf', category: 'document' },\n} as const;\n\nexport type AllowedMimeType = keyof typeof ALLOWED_FILE_TYPES;\n\nexport const ALLOWED_MIME_TYPES = Object.keys(ALLOWED_FILE_TYPES) as AllowedMimeType[];\n\nexport const IMAGE_MIME_TYPES = ALLOWED_MIME_TYPES.filter(\n (type) => ALLOWED_FILE_TYPES[type].category === 'image'\n);\n\nexport const DOCUMENT_MIME_TYPES = ALLOWED_MIME_TYPES.filter(\n (type) => ALLOWED_FILE_TYPES[type].category === 'document'\n);\n\n/**\n * Processing contexts\n */\nexport const PROCESSING_CONTEXTS = ['newsletter', 'invoice', 'framer-site', 'default'] as const;\nexport type ProcessingContext = (typeof PROCESSING_CONTEXTS)[number];\n\n/**\n * File size limits\n */\nexport const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024; // 100MB per file\n\n/**\n * Thumbnail sizes\n */\nexport const THUMBNAIL_SIZES = {\n thumb: { width: 200, height: 200 },\n medium: { width: 400, height: 400 },\n large: { width: 800, height: 800 },\n} as const;\n\nexport type ThumbnailSize = keyof typeof THUMBNAIL_SIZES;\n\n/**\n * Processing statuses\n */\nexport const PROCESSING_STATUSES = ['pending', 'processing', 'completed', 'failed'] as const;\nexport type ProcessingStatus = (typeof PROCESSING_STATUSES)[number];\n\n/**\n * Retry configuration\n */\nexport const RETRY_CONFIG = {\n maxAttempts: 3,\n initialDelayMs: 1000,\n maxDelayMs: 10000,\n backoffMultiplier: 2,\n} as const;\n","/**\n * Base error class for Storage Brain SDK\n */\nexport class StorageBrainError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'StorageBrainError';\n }\n}\n\n/**\n * Authentication error - invalid or missing API key\n */\nexport class AuthenticationError extends StorageBrainError {\n constructor(message = 'Authentication failed') {\n super(message, 'AUTHENTICATION_ERROR', 401);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Quota exceeded error\n */\nexport class QuotaExceededError extends StorageBrainError {\n constructor(\n message = 'Storage quota exceeded',\n public quotaBytes?: number,\n public usedBytes?: number\n ) {\n super(message, 'QUOTA_EXCEEDED', 403, { quotaBytes, usedBytes });\n this.name = 'QuotaExceededError';\n }\n}\n\n/**\n * Invalid file type error\n */\nexport class InvalidFileTypeError extends StorageBrainError {\n constructor(\n fileType: string,\n allowedTypes?: string[]\n ) {\n super(\n `File type '${fileType}' is not allowed`,\n 'INVALID_FILE_TYPE',\n 400,\n { fileType, allowedTypes }\n );\n this.name = 'InvalidFileTypeError';\n }\n}\n\n/**\n * File too large error\n */\nexport class FileTooLargeError extends StorageBrainError {\n constructor(\n fileSize: number,\n maxSize: number\n ) {\n super(\n `File size ${fileSize} bytes exceeds maximum of ${maxSize} bytes`,\n 'FILE_TOO_LARGE',\n 400,\n { fileSize, maxSize }\n );\n this.name = 'FileTooLargeError';\n }\n}\n\n/**\n * File not found error\n */\nexport class FileNotFoundError extends StorageBrainError {\n constructor(fileId: string) {\n super(`File not found: ${fileId}`, 'FILE_NOT_FOUND', 404, { fileId });\n this.name = 'FileNotFoundError';\n }\n}\n\n/**\n * Network error - connection issues\n */\nexport class NetworkError extends StorageBrainError {\n constructor(message = 'Network error occurred', public originalError?: Error) {\n super(message, 'NETWORK_ERROR', undefined, { originalError: originalError?.message });\n this.name = 'NetworkError';\n }\n}\n\n/**\n * Upload error - file upload failed\n */\nexport class UploadError extends StorageBrainError {\n constructor(message: string, public originalError?: Error) {\n super(message, 'UPLOAD_ERROR', undefined, { originalError: originalError?.message });\n this.name = 'UploadError';\n }\n}\n\n/**\n * Validation error - request validation failed\n */\nexport class ValidationError extends StorageBrainError {\n constructor(\n message: string,\n public errors?: Array<{ path: string; message: string }>\n ) {\n super(message, 'VALIDATION_ERROR', 400, { errors });\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Parse API error response into appropriate error class\n */\nexport function parseApiError(\n statusCode: number,\n response: { error?: { code?: string; message?: string; details?: Record<string, unknown> } }\n): StorageBrainError {\n const { code, message, details } = response.error ?? {};\n\n switch (code) {\n case 'UNAUTHORIZED':\n return new AuthenticationError(message);\n case 'QUOTA_EXCEEDED':\n return new QuotaExceededError(\n message,\n details?.quotaBytes as number,\n details?.usedBytes as number\n );\n case 'INVALID_FILE_TYPE':\n return new InvalidFileTypeError(\n details?.fileType as string,\n details?.allowedTypes as string[]\n );\n case 'FILE_TOO_LARGE':\n return new FileTooLargeError(\n details?.fileSize as number,\n details?.maxSize as number\n );\n case 'FILE_NOT_FOUND':\n case 'NOT_FOUND':\n return new FileNotFoundError(details?.fileId as string ?? 'unknown');\n case 'VALIDATION_ERROR':\n return new ValidationError(\n message ?? 'Validation failed',\n details?.errors as Array<{ path: string; message: string }>\n );\n default:\n return new StorageBrainError(\n message ?? 'An error occurred',\n code ?? 'UNKNOWN_ERROR',\n statusCode,\n details\n );\n }\n}\n","import type { AllowedMimeType } from './constants';\nimport { ALLOWED_MIME_TYPES, RETRY_CONFIG } from './constants';\nimport type {\n StorageBrainConfig,\n UploadOptions,\n FileInfo,\n ListFilesOptions,\n ListFilesResult,\n QuotaInfo,\n TenantInfo,\n UploadHandshake,\n} from './types';\nimport {\n StorageBrainError,\n NetworkError,\n UploadError,\n InvalidFileTypeError,\n parseApiError,\n} from './errors';\n\nconst DEFAULT_BASE_URL = 'https://storage-brain-api.marlin-pohl.workers.dev';\nconst DEFAULT_TIMEOUT = 30000;\nconst DEFAULT_MAX_RETRIES = 3;\n\n/**\n * Storage Brain SDK Client\n *\n * @example\n * ```typescript\n * const storage = new StorageBrain({\n * apiKey: 'sk_live_...',\n * });\n *\n * // Upload a file\n * const file = await storage.upload(fileBlob, {\n * context: 'newsletter',\n * onProgress: (p) => console.log(`${p}%`),\n * });\n *\n * console.log(file.url);\n * ```\n */\nexport class StorageBrain {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeout: number;\n private readonly maxRetries: number;\n\n constructor(config: StorageBrainConfig) {\n if (!config.apiKey) {\n throw new StorageBrainError('API key is required', 'CONFIGURATION_ERROR');\n }\n\n this.apiKey = config.apiKey;\n this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '');\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n /**\n * Upload a file\n */\n async upload(file: File | Blob, options: UploadOptions): Promise<FileInfo> {\n const { context, tags, onProgress, webhookUrl, signal } = options;\n\n // Get file info\n const fileName = file instanceof File ? file.name : 'file';\n const fileType = file.type as AllowedMimeType;\n const fileSize = file.size;\n\n // Validate file type\n if (!ALLOWED_MIME_TYPES.includes(fileType)) {\n throw new InvalidFileTypeError(fileType, [...ALLOWED_MIME_TYPES]);\n }\n\n // Request upload handshake\n const handshake = await this.requestUpload({\n fileType,\n fileName,\n fileSizeBytes: fileSize,\n context,\n tags,\n webhookUrl,\n });\n\n onProgress?.(10);\n\n // Upload file to presigned URL\n await this.uploadToPresignedUrl(handshake.presignedUrl, file, fileType, (progress) => {\n // Map upload progress to 10-90%\n onProgress?.(10 + Math.round(progress * 0.8));\n }, signal);\n\n onProgress?.(90);\n\n // Poll for file status (processing may take time)\n const fileInfo = await this.waitForProcessing(handshake.fileId, signal);\n\n onProgress?.(100);\n\n return fileInfo;\n }\n\n /**\n * Request an upload handshake\n */\n private async requestUpload(params: {\n fileType: AllowedMimeType;\n fileName: string;\n fileSizeBytes: number;\n context: string;\n tags?: Record<string, string>;\n webhookUrl?: string;\n }): Promise<UploadHandshake> {\n return this.request<UploadHandshake>('POST', '/api/v1/upload/request', params);\n }\n\n /**\n * Upload file content to presigned URL\n */\n private async uploadToPresignedUrl(\n presignedUrl: string,\n file: File | Blob,\n contentType: string,\n onProgress?: (progress: number) => void,\n signal?: AbortSignal\n ): Promise<void> {\n // Determine if URL is relative (our internal endpoint) or absolute (direct R2)\n const uploadUrl = presignedUrl.startsWith('/')\n ? `${this.baseUrl}${presignedUrl}`\n : presignedUrl;\n\n try {\n // Use XMLHttpRequest for progress tracking\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n xhr.upload.addEventListener('progress', (event) => {\n if (event.lengthComputable && onProgress) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n });\n\n xhr.addEventListener('load', () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new UploadError(`Upload failed with status ${xhr.status}`));\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new NetworkError('Network error during upload'));\n });\n\n xhr.addEventListener('abort', () => {\n reject(new UploadError('Upload was cancelled'));\n });\n\n if (signal) {\n signal.addEventListener('abort', () => {\n xhr.abort();\n });\n }\n\n xhr.open('PUT', uploadUrl);\n xhr.setRequestHeader('Content-Type', contentType);\n\n // Add auth header for internal endpoint\n if (presignedUrl.startsWith('/')) {\n xhr.setRequestHeader('Authorization', `Bearer ${this.apiKey}`);\n }\n\n xhr.send(file);\n });\n } catch (error) {\n if (error instanceof StorageBrainError) {\n throw error;\n }\n throw new UploadError(\n 'Failed to upload file',\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Wait for file processing to complete\n */\n private async waitForProcessing(\n fileId: string,\n signal?: AbortSignal,\n maxWaitMs = 60000,\n pollIntervalMs = 1000\n ): Promise<FileInfo> {\n const startTime = Date.now();\n\n while (Date.now() - startTime < maxWaitMs) {\n if (signal?.aborted) {\n throw new UploadError('Operation was cancelled');\n }\n\n const file = await this.getFile(fileId);\n\n if (file.processingStatus === 'completed' || file.processingStatus === 'failed') {\n return file;\n }\n\n // Wait before polling again\n await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));\n }\n\n // Return current state even if not fully processed\n return this.getFile(fileId);\n }\n\n /**\n * Get a file by ID\n */\n async getFile(fileId: string): Promise<FileInfo> {\n return this.request<FileInfo>('GET', `/api/v1/files/${fileId}`);\n }\n\n /**\n * List files\n */\n async listFiles(options?: ListFilesOptions): Promise<ListFilesResult> {\n const params = new URLSearchParams();\n\n if (options?.limit) params.set('limit', options.limit.toString());\n if (options?.cursor) params.set('cursor', options.cursor);\n if (options?.context) params.set('context', options.context);\n if (options?.fileType) params.set('fileType', options.fileType);\n\n const query = params.toString();\n const path = query ? `/api/v1/files?${query}` : '/api/v1/files';\n\n return this.request<ListFilesResult>('GET', path);\n }\n\n /**\n * Delete a file\n */\n async deleteFile(fileId: string): Promise<void> {\n await this.request<{ success: boolean }>('DELETE', `/api/v1/files/${fileId}`);\n }\n\n /**\n * Get quota usage\n */\n async getQuota(): Promise<QuotaInfo> {\n return this.request<QuotaInfo>('GET', '/api/v1/tenant/quota');\n }\n\n /**\n * Get tenant information\n */\n async getTenantInfo(): Promise<TenantInfo> {\n return this.request<TenantInfo>('GET', '/api/v1/tenant/info');\n }\n\n /**\n * Make an authenticated API request with retry logic\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < this.maxRetries; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n const response = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorBody = await response.json().catch(() => ({}));\n throw parseApiError(response.status, errorBody as { error?: { code?: string; message?: string; details?: Record<string, unknown> } });\n }\n\n return await response.json() as T;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry on client errors (4xx) or specific errors\n if (error instanceof StorageBrainError && error.statusCode && error.statusCode < 500) {\n throw error;\n }\n\n // Exponential backoff for retries\n if (attempt < this.maxRetries - 1) {\n const delay = Math.min(\n RETRY_CONFIG.initialDelayMs * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt),\n RETRY_CONFIG.maxDelayMs\n );\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw new NetworkError(\n `Request failed after ${this.maxRetries} attempts`,\n lastError\n );\n }\n}\n\n// Default export for convenience\nexport default StorageBrain;\n"]}