@bigbucket-ai/sdk 0.0.0-stage → 0.2.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/dist/index.mjs ADDED
@@ -0,0 +1,516 @@
1
+ // src/errors.ts
2
+ var BigBucketError = class extends Error {
3
+ statusCode;
4
+ code;
5
+ requestId;
6
+ details;
7
+ isRetryable;
8
+ constructor(options) {
9
+ super(options.message);
10
+ this.name = "BigBucketError";
11
+ this.statusCode = options.statusCode ?? 500;
12
+ this.code = options.code ?? "INTERNAL_SERVER_ERROR";
13
+ this.requestId = options.requestId;
14
+ this.details = options.details;
15
+ this.cause = options.cause;
16
+ this.isRetryable = this.statusCode === 429 || this.statusCode >= 500 && this.statusCode <= 599;
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ };
20
+ var AuthenticationError = class extends BigBucketError {
21
+ constructor(options) {
22
+ super({ ...options, statusCode: options.statusCode ?? 401 });
23
+ this.name = "AuthenticationError";
24
+ }
25
+ };
26
+ var NotFoundError = class extends BigBucketError {
27
+ constructor(options) {
28
+ super({ ...options, statusCode: 404 });
29
+ this.name = "NotFoundError";
30
+ }
31
+ };
32
+ var PayloadTooLargeError = class extends BigBucketError {
33
+ constructor(options) {
34
+ super({ ...options, statusCode: 413 });
35
+ this.name = "PayloadTooLargeError";
36
+ }
37
+ };
38
+ var UnsupportedFormatError = class extends BigBucketError {
39
+ constructor(options) {
40
+ super({ ...options, statusCode: 415 });
41
+ this.name = "UnsupportedFormatError";
42
+ }
43
+ };
44
+ var RateLimitError = class extends BigBucketError {
45
+ constructor(options) {
46
+ super({ ...options, statusCode: 429 });
47
+ this.name = "RateLimitError";
48
+ }
49
+ };
50
+ var StorageUploadError = class extends BigBucketError {
51
+ constructor(message, cause) {
52
+ super({
53
+ message,
54
+ statusCode: 502,
55
+ code: "STORAGE_UPLOAD_FAILED",
56
+ cause
57
+ });
58
+ this.name = "StorageUploadError";
59
+ }
60
+ };
61
+ var TimeoutError = class extends BigBucketError {
62
+ constructor(message = "Request timed out") {
63
+ super({
64
+ message,
65
+ statusCode: 408,
66
+ code: "REQUEST_TIMEOUT"
67
+ });
68
+ this.name = "TimeoutError";
69
+ }
70
+ };
71
+
72
+ // src/http/transport.ts
73
+ var HttpTransport = class {
74
+ apiKey;
75
+ baseUrl;
76
+ timeout;
77
+ customFetch;
78
+ constructor(config) {
79
+ if (!config.apiKey || typeof config.apiKey !== "string") {
80
+ throw new Error("A valid BigBucket API key is required (e.g. bb_live_...)");
81
+ }
82
+ this.apiKey = config.apiKey.trim();
83
+ this.baseUrl = (config.baseUrl || "http://localhost:8000/api/v1").replace(/\/+$/, "");
84
+ this.timeout = config.timeout ?? 3e4;
85
+ this.customFetch = config.fetch ?? globalThis.fetch;
86
+ if (!this.customFetch) {
87
+ throw new Error("No native fetch implementation found. Pass a custom fetch in options.");
88
+ }
89
+ }
90
+ /**
91
+ * Execute an authenticated request against the BigBucket API.
92
+ */
93
+ async request(path, options = {}) {
94
+ const url = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
95
+ const headers = {
96
+ Accept: "application/json",
97
+ "X-API-Key": this.apiKey,
98
+ ...options.headers
99
+ };
100
+ if (options.body && typeof options.body === "string" && !headers["Content-Type"]) {
101
+ headers["Content-Type"] = "application/json";
102
+ }
103
+ const controller = new AbortController();
104
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
105
+ let response;
106
+ try {
107
+ response = await this.customFetch(url, {
108
+ ...options,
109
+ headers,
110
+ signal: controller.signal
111
+ });
112
+ } catch (err) {
113
+ clearTimeout(timeoutId);
114
+ if (err instanceof Error && err.name === "AbortError") {
115
+ throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
116
+ }
117
+ throw new BigBucketError({
118
+ message: `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
119
+ statusCode: 0,
120
+ cause: err
121
+ });
122
+ } finally {
123
+ clearTimeout(timeoutId);
124
+ }
125
+ const requestId = response.headers.get("X-Request-ID") ?? void 0;
126
+ let responseBody = null;
127
+ const contentType = response.headers.get("content-type");
128
+ if (contentType && contentType.includes("application/json")) {
129
+ try {
130
+ responseBody = await response.json();
131
+ } catch {
132
+ }
133
+ }
134
+ if (!response.ok) {
135
+ const errPayload = responseBody?.error ?? {};
136
+ const message = errPayload.message || `Request failed with status ${response.status}`;
137
+ const code = errPayload.code || "REQUEST_FAILED";
138
+ const details = errPayload.details;
139
+ const errorOptions = {
140
+ message,
141
+ statusCode: response.status,
142
+ code,
143
+ requestId: errPayload.request_id || requestId,
144
+ details
145
+ };
146
+ if (response.status === 401) throw new AuthenticationError(errorOptions);
147
+ if (response.status === 404) throw new NotFoundError(errorOptions);
148
+ if (response.status === 413) throw new PayloadTooLargeError(errorOptions);
149
+ if (response.status === 415) throw new UnsupportedFormatError(errorOptions);
150
+ if (response.status === 429) throw new RateLimitError(errorOptions);
151
+ throw new BigBucketError(errorOptions);
152
+ }
153
+ return responseBody;
154
+ }
155
+ /**
156
+ * Upload binary data directly to the storage provider (Cloudflare R2 or Local Storage).
157
+ * Note: Permanent BigBucket credentials MUST NOT be forwarded to the storage provider.
158
+ */
159
+ async uploadToPresignedUrl(uploadUrl, method, signedHeaders, body) {
160
+ const controller = new AbortController();
161
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout * 2);
162
+ try {
163
+ const response = await this.customFetch(uploadUrl, {
164
+ method: method || "PUT",
165
+ headers: signedHeaders,
166
+ body,
167
+ signal: controller.signal
168
+ });
169
+ if (!response.ok) {
170
+ throw new StorageUploadError(
171
+ `Direct storage upload failed with status ${response.status} ${response.statusText}`
172
+ );
173
+ }
174
+ } catch (err) {
175
+ clearTimeout(timeoutId);
176
+ if (err instanceof BigBucketError) {
177
+ throw err;
178
+ }
179
+ if (err instanceof Error && err.name === "AbortError") {
180
+ throw new TimeoutError(`Direct storage upload timed out after ${this.timeout * 2}ms`);
181
+ }
182
+ throw new StorageUploadError(
183
+ `Failed to upload image bytes to storage provider: ${err instanceof Error ? err.message : String(err)}`,
184
+ err
185
+ );
186
+ } finally {
187
+ clearTimeout(timeoutId);
188
+ }
189
+ }
190
+ };
191
+
192
+ // src/resources/images.ts
193
+ var ImagesResource = class {
194
+ constructor(transport) {
195
+ this.transport = transport;
196
+ }
197
+ transport;
198
+ mapAsset(d) {
199
+ return {
200
+ id: d.id,
201
+ projectId: d.project_id,
202
+ url: d.url,
203
+ originalFilename: d.original_filename,
204
+ mimeType: d.mime_type,
205
+ fileSize: d.file_size,
206
+ width: d.width,
207
+ height: d.height,
208
+ format: d.format,
209
+ status: d.status,
210
+ createdAt: d.created_at,
211
+ updatedAt: d.updated_at,
212
+ variants: (d.variants || []).map((v) => ({
213
+ variantName: v.variant_name,
214
+ url: v.url,
215
+ mimeType: v.mime_type,
216
+ fileSize: v.file_size,
217
+ width: v.width,
218
+ height: v.height,
219
+ format: v.format
220
+ }))
221
+ };
222
+ }
223
+ /**
224
+ * Retrieve metadata and stable delivery URL for an image.
225
+ */
226
+ async get(imageId) {
227
+ const res = await this.transport.request(
228
+ `/images/${encodeURIComponent(imageId)}`,
229
+ {
230
+ method: "GET"
231
+ }
232
+ );
233
+ return this.mapAsset(res.data);
234
+ }
235
+ /**
236
+ * List and paginate media assets belonging to the authenticated project.
237
+ */
238
+ async list(options = {}) {
239
+ const params = new URLSearchParams();
240
+ if (options.page !== void 0) params.set("page", String(options.page));
241
+ if (options.pageSize !== void 0) params.set("page_size", String(options.pageSize));
242
+ if (options.status !== void 0) params.set("status", options.status);
243
+ if (options.format !== void 0) params.set("format", options.format);
244
+ const query = params.toString() ? `?${params.toString()}` : "";
245
+ const res = await this.transport.request(
246
+ `/images${query}`,
247
+ {
248
+ method: "GET"
249
+ }
250
+ );
251
+ return {
252
+ data: (res.data || []).map((item) => this.mapAsset(item)),
253
+ pagination: {
254
+ total: res.pagination.total,
255
+ page: res.pagination.page,
256
+ pageSize: res.pagination.page_size,
257
+ hasNext: res.pagination.has_next
258
+ }
259
+ };
260
+ }
261
+ /**
262
+ * Delete an image from storage and mark it deleted in BigBucket.
263
+ */
264
+ async delete(imageId) {
265
+ const res = await this.transport.request(
266
+ `/images/${encodeURIComponent(imageId)}`,
267
+ {
268
+ method: "DELETE"
269
+ }
270
+ );
271
+ return {
272
+ success: true,
273
+ id: res.data.id
274
+ };
275
+ }
276
+ };
277
+
278
+ // src/resources/uploads.ts
279
+ var UploadsResource = class {
280
+ constructor(transport) {
281
+ this.transport = transport;
282
+ }
283
+ transport;
284
+ /**
285
+ * Request permission and generate a presigned upload URL.
286
+ */
287
+ async create(options) {
288
+ const res = await this.transport.request("/uploads", {
289
+ method: "POST",
290
+ body: JSON.stringify({
291
+ filename: options.filename,
292
+ content_type: options.contentType,
293
+ file_size: options.fileSize
294
+ })
295
+ });
296
+ const d = res.data;
297
+ return {
298
+ uploadId: d.upload_id,
299
+ objectKey: d.object_key,
300
+ presigned: {
301
+ url: d.presigned.url,
302
+ method: d.presigned.method,
303
+ headers: d.presigned.headers
304
+ },
305
+ expiresAt: d.expires_at
306
+ };
307
+ }
308
+ /**
309
+ * Finalize an upload session once the client has uploaded bytes to the presigned URL.
310
+ */
311
+ async complete(uploadId) {
312
+ const res = await this.transport.request(
313
+ `/uploads/${encodeURIComponent(uploadId)}/complete`,
314
+ {
315
+ method: "POST"
316
+ }
317
+ );
318
+ const d = res.data;
319
+ return {
320
+ id: d.id,
321
+ projectId: d.project_id,
322
+ url: d.url,
323
+ originalFilename: d.original_filename,
324
+ mimeType: d.mime_type,
325
+ fileSize: d.file_size,
326
+ width: d.width,
327
+ height: d.height,
328
+ format: d.format,
329
+ status: d.status,
330
+ createdAt: d.created_at,
331
+ updatedAt: d.updated_at,
332
+ variants: (d.variants || []).map((v) => ({
333
+ variantName: v.variant_name,
334
+ url: v.url,
335
+ mimeType: v.mime_type,
336
+ fileSize: v.file_size,
337
+ width: v.width,
338
+ height: v.height,
339
+ format: v.format
340
+ }))
341
+ };
342
+ }
343
+ /**
344
+ * Check the status of an ongoing or completed upload session.
345
+ */
346
+ async get(uploadId) {
347
+ const res = await this.transport.request(
348
+ `/uploads/${encodeURIComponent(uploadId)}`,
349
+ {
350
+ method: "GET"
351
+ }
352
+ );
353
+ const d = res.data;
354
+ return {
355
+ id: d.id,
356
+ projectId: d.project_id,
357
+ objectKey: d.object_key,
358
+ expectedFileSize: d.expected_file_size,
359
+ expectedMimeType: d.expected_mime_type,
360
+ status: d.status,
361
+ expiresAt: d.expires_at,
362
+ createdAt: d.created_at,
363
+ completedAt: d.completed_at ?? null,
364
+ assetId: d.asset_id ?? null
365
+ };
366
+ }
367
+ };
368
+
369
+ // src/resources/usage.ts
370
+ var UsageResource = class {
371
+ constructor(transport) {
372
+ this.transport = transport;
373
+ }
374
+ transport;
375
+ /**
376
+ * Retrieve usage metrics for a project or the account.
377
+ */
378
+ async get(projectId) {
379
+ const path = projectId ? `/projects/${encodeURIComponent(projectId)}/usage` : "/usage";
380
+ const res = await this.transport.request(path, {
381
+ method: "GET"
382
+ });
383
+ const d = res.data;
384
+ return {
385
+ totalStoredBytes: d.total_stored_bytes,
386
+ totalReadyImages: d.total_ready_images,
387
+ successfulUploads: d.successful_uploads,
388
+ deletedImages: d.deleted_images
389
+ };
390
+ }
391
+ };
392
+
393
+ // src/transform.ts
394
+ function buildTransformUrl(baseUrl, options) {
395
+ if (!baseUrl) return "";
396
+ if (!options || Object.keys(options).length === 0) return baseUrl;
397
+ const url = new URL(baseUrl);
398
+ if (options.tr) {
399
+ url.searchParams.set("tr", options.tr);
400
+ }
401
+ const w = options.width ?? options.w;
402
+ if (w !== void 0) {
403
+ url.searchParams.set("w", String(w));
404
+ }
405
+ const h = options.height ?? options.h;
406
+ if (h !== void 0) {
407
+ url.searchParams.set("h", String(h));
408
+ }
409
+ if (options.fit) {
410
+ url.searchParams.set("fit", options.fit);
411
+ }
412
+ const q = options.quality ?? options.q;
413
+ if (q !== void 0) {
414
+ url.searchParams.set("q", String(q));
415
+ }
416
+ const fmt = options.format ?? options.fmt;
417
+ if (fmt) {
418
+ url.searchParams.set("fmt", fmt);
419
+ }
420
+ return url.toString();
421
+ }
422
+
423
+ // src/client.ts
424
+ var BigBucket = class {
425
+ transport;
426
+ images;
427
+ uploads;
428
+ usage;
429
+ constructor(options) {
430
+ this.transport = new HttpTransport({
431
+ apiKey: options.apiKey,
432
+ baseUrl: options.baseUrl,
433
+ timeout: options.timeout,
434
+ fetch: options.fetch
435
+ });
436
+ this.images = new ImagesResource(this.transport);
437
+ this.uploads = new UploadsResource(this.transport);
438
+ this.usage = new UsageResource(this.transport);
439
+ }
440
+ /**
441
+ * Helper method to normalize arbitrary input types (Buffer, Uint8Array, Blob, etc.)
442
+ * and resolve byte length.
443
+ */
444
+ resolveFileData(file, explicitSize) {
445
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
446
+ return { body: file, size: explicitSize ?? file.size };
447
+ }
448
+ if (file instanceof ArrayBuffer) {
449
+ return { body: file, size: explicitSize ?? file.byteLength };
450
+ }
451
+ if (file instanceof Uint8Array) {
452
+ return { body: file, size: explicitSize ?? file.byteLength };
453
+ }
454
+ if (typeof file === "object" && file !== null && "buffer" in file && file.buffer instanceof ArrayBuffer) {
455
+ const buffer = file.buffer;
456
+ return { body: buffer, size: explicitSize ?? buffer.byteLength };
457
+ }
458
+ if (typeof file === "string") {
459
+ const encoded = new TextEncoder().encode(file);
460
+ return { body: encoded, size: explicitSize ?? encoded.byteLength };
461
+ }
462
+ if (explicitSize !== void 0) {
463
+ return { body: file, size: explicitSize };
464
+ }
465
+ throw new Error(
466
+ "Unable to determine file size. Please provide 'options.fileSize' explicitly."
467
+ );
468
+ }
469
+ /**
470
+ * Complete end-to-end image upload workflow:
471
+ * 1. Requests an upload session and receives presigned storage URL.
472
+ * 2. Uploads binary data directly to the storage provider (Cloudflare R2).
473
+ * 3. Finalizes the upload with server verification and content inspection.
474
+ * 4. Returns verified ImageAsset with delivery URL.
475
+ */
476
+ async upload(file, options) {
477
+ const { body, size } = this.resolveFileData(file, options.fileSize);
478
+ const session = await this.uploads.create({
479
+ filename: options.filename,
480
+ contentType: options.contentType,
481
+ fileSize: size
482
+ });
483
+ await this.transport.uploadToPresignedUrl(
484
+ session.presigned.url,
485
+ session.presigned.method,
486
+ session.presigned.headers,
487
+ body
488
+ );
489
+ return await this.uploads.complete(session.uploadId);
490
+ }
491
+ /**
492
+ * Helper method to generate an optimized, transformed image delivery URL.
493
+ *
494
+ * @param baseUrl The original delivery URL of the image asset.
495
+ * @param options Dynamic transformation parameters (width, height, quality, format, or Zepto shorthand).
496
+ */
497
+ transformUrl(baseUrl, options) {
498
+ return buildTransformUrl(baseUrl, options);
499
+ }
500
+ };
501
+ export {
502
+ AuthenticationError,
503
+ BigBucket,
504
+ BigBucketError,
505
+ HttpTransport,
506
+ ImagesResource,
507
+ NotFoundError,
508
+ PayloadTooLargeError,
509
+ RateLimitError,
510
+ StorageUploadError,
511
+ TimeoutError,
512
+ UnsupportedFormatError,
513
+ UploadsResource,
514
+ UsageResource,
515
+ buildTransformUrl
516
+ };
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@bigbucket-ai/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Official TypeScript/JavaScript SDK for the BigBucket Image Storage and Management Platform",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
27
+ "test": "vitest run",
28
+ "type-check": "tsc --noEmit",
29
+ "lint": "eslint src/ --ext .ts"
30
+ },
31
+ "keywords": [
32
+ "bigbucket",
33
+ "image-storage",
34
+ "image-upload",
35
+ "r2",
36
+ "cloudflare-r2",
37
+ "fastapi",
38
+ "sdk"
39
+ ],
40
+ "author": "BigBucket Team",
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20.14.0",
50
+ "tsup": "^8.1.0",
51
+ "typescript": "^5.4.5",
52
+ "vitest": "^1.6.0"
53
+ }
54
+ }