@techfinityedge/koolbase-react-native 5.1.1 → 5.3.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 CHANGED
@@ -73,6 +73,8 @@ const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
73
73
  });
74
74
  ```
75
75
 
76
+ ---
77
+
76
78
  ### OAuth — Apple
77
79
 
78
80
  Apple Sign-In uses the native authentication flow via `@invertase/react-native-apple-authentication` as a peer dependency:
@@ -100,6 +102,8 @@ const session = await Koolbase.auth.signInWithApple({
100
102
 
101
103
  Configure Apple Sign-In for your environment with your iOS app's Bundle ID. Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
102
104
 
105
+ ---
106
+
103
107
  ### OAuth — Google
104
108
 
105
109
  Google Sign-In uses the native authentication flow via `@react-native-google-signin/google-signin` as a peer dependency:
@@ -121,6 +125,8 @@ const session = await Koolbase.auth.signInWithGoogle({
121
125
 
122
126
  Configure Google Sign-In for your environment with the OAuth client IDs from Google Cloud Console (typically one each for iOS, Android, and web). Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
123
127
 
128
+ ---
129
+
124
130
  ### Phone + OTP
125
131
 
126
132
  ```typescript
@@ -173,6 +179,8 @@ await Koolbase.db.update('record-id', { title: 'Updated' });
173
179
  await Koolbase.db.delete('record-id');
174
180
  ```
175
181
 
182
+ ---
183
+
176
184
  ### Handling unique-constraint conflicts
177
185
 
178
186
  A write that would violate a unique constraint throws `KoolbaseConflictError`:
@@ -187,6 +195,46 @@ try {
187
195
  }
188
196
  ```
189
197
 
198
+ ---
199
+
200
+ ### Public bucket URLs
201
+
202
+ For files in public buckets, you can construct the stable CDN URL directly — no
203
+ network call, no expiry, embeddable anywhere a browser fetches a URL.
204
+
205
+ ```typescript
206
+ import { KoolbaseStorage } from '@techfinityedge/koolbase-react-native';
207
+
208
+ // From a KoolbaseObject you already have (e.g. from upload() or another read)
209
+ const { object } = await Koolbase.storage.upload({
210
+ bucket: 'avatars',
211
+ path: `user-${userId}.jpg`,
212
+ file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
213
+ });
214
+
215
+ const url = KoolbaseStorage.publicUrlForObject(object, 'avatars');
216
+ // url is null for private-bucket objects; the CDN URL for public-bucket ones.
217
+
218
+ if (url) {
219
+ // Safe to use — file lives in the public R2 bucket
220
+ return <Image source={{ uri: url }} />;
221
+ }
222
+
223
+ // For build-time URL construction (no Object on hand)
224
+ const url = KoolbaseStorage.publicUrl({
225
+ projectId: 'proj_abc',
226
+ bucket: 'avatars',
227
+ path: 'user-123.jpg',
228
+ });
229
+ // Always returns the URL pattern; caller is responsible for knowing
230
+ // the file lives in a public bucket. For files in private buckets,
231
+ // the resulting URL will 404.
232
+ ```
233
+
234
+ URLs follow the pattern `https://cdn.koolbase.com/{project_id}/{bucket}/{path}` — long-lived, edge-cached, no authentication. For files in private buckets, use `getDownloadUrl` instead, which returns a 1-hour presigned URL.
235
+
236
+ ---
237
+
190
238
  ### Upsert
191
239
 
192
240
  Insert a record, or update the existing one matching a filter.
@@ -230,6 +278,8 @@ if (isFromCache) console.log('Served from local cache');
230
278
  await Koolbase.db.syncPendingWrites();
231
279
  ```
232
280
 
281
+ ---
282
+
233
283
  ### Atomic batch writes
234
284
 
235
285
  Run multiple writes in a single server-side transaction. All operations commit together or none are applied — any failure rolls back the entire batch.
@@ -309,6 +359,8 @@ const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg
309
359
  await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
310
360
  ```
311
361
 
362
+ ---
363
+
312
364
  ### Handling upload conflicts
313
365
 
314
366
  For user-supplied filenames, prompt the user before overwriting:
@@ -622,6 +674,8 @@ try {
622
674
  > the background, so their conflicts surface via the sync engine, not as a
623
675
  > thrown error.
624
676
 
677
+ ---
678
+
625
679
  ### Storage errors
626
680
 
627
681
  All storage failures extend `KoolbaseStorageError` (which extends `Error`):
@@ -103,6 +103,46 @@ export declare class KoolbaseStorageFileTooLargeError extends KoolbaseStorageErr
103
103
  export declare class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
104
104
  constructor(message?: string);
105
105
  }
106
+ /**
107
+ * Thrown when an object metadata payload (either at upload-confirm time
108
+ * or via `updateMetadata`) fails server-side validation — the server
109
+ * responds with 400 and code `metadata_invalid`.
110
+ *
111
+ * The `detail` field carries the specific reason from the server — e.g.
112
+ * `'key "foo bar": must match [a-z0-9_]+'`, `'exceeds 50 keys (got 53)'`,
113
+ * or `'exceeds 8192 bytes total (sum of all key + value lengths)'`. The
114
+ * detail names the failing key and rule so callers can fix the offending
115
+ * entry without guessing what shape rule was violated.
116
+ *
117
+ * Validation rules (enforced server-side):
118
+ * - At most 50 keys per object.
119
+ * - At most 8KB total (sum of byte lengths across all keys + values).
120
+ * - Keys: 1–64 chars, must match `[a-z0-9_]+`.
121
+ * - Keys with a leading underscore are reserved for system use.
122
+ * - Values: at most 1024 chars each.
123
+ *
124
+ * @example
125
+ * try {
126
+ * await Koolbase.storage.updateMetadata('photos', 'sunset.jpg', {
127
+ * tag: 'sunset',
128
+ * 'BAD KEY': 'oops',
129
+ * });
130
+ * } catch (e) {
131
+ * if (e instanceof KoolbaseStorageMetadataInvalidError) {
132
+ * console.warn('Metadata rejected:', e.detail);
133
+ * // -> 'Metadata rejected: key "BAD KEY": must match [a-z0-9_]+'
134
+ * }
135
+ * }
136
+ */
137
+ export declare class KoolbaseStorageMetadataInvalidError extends KoolbaseStorageError {
138
+ /**
139
+ * The specific validation failure reported by the server. Names the
140
+ * failing key (when applicable) and the rule that was violated.
141
+ * Surface this directly to developer logs or user-facing UI.
142
+ */
143
+ detail?: string;
144
+ constructor(message?: string, detail?: string);
145
+ }
106
146
  /**
107
147
  * Maps a non-2xx storage-layer response to a typed
108
148
  * {@link KoolbaseStorageError}, preferring the server's stable `code` and
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageError = void 0;
3
+ exports.KoolbaseStorageMetadataInvalidError = exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageError = void 0;
4
4
  exports.koolbaseStorageError = koolbaseStorageError;
5
5
  exports.koolbaseStorageErrorFromResponse = koolbaseStorageErrorFromResponse;
6
6
  /**
@@ -148,6 +148,46 @@ class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
148
148
  }
149
149
  }
150
150
  exports.KoolbaseStorageMimeTypeError = KoolbaseStorageMimeTypeError;
151
+ /**
152
+ * Thrown when an object metadata payload (either at upload-confirm time
153
+ * or via `updateMetadata`) fails server-side validation — the server
154
+ * responds with 400 and code `metadata_invalid`.
155
+ *
156
+ * The `detail` field carries the specific reason from the server — e.g.
157
+ * `'key "foo bar": must match [a-z0-9_]+'`, `'exceeds 50 keys (got 53)'`,
158
+ * or `'exceeds 8192 bytes total (sum of all key + value lengths)'`. The
159
+ * detail names the failing key and rule so callers can fix the offending
160
+ * entry without guessing what shape rule was violated.
161
+ *
162
+ * Validation rules (enforced server-side):
163
+ * - At most 50 keys per object.
164
+ * - At most 8KB total (sum of byte lengths across all keys + values).
165
+ * - Keys: 1–64 chars, must match `[a-z0-9_]+`.
166
+ * - Keys with a leading underscore are reserved for system use.
167
+ * - Values: at most 1024 chars each.
168
+ *
169
+ * @example
170
+ * try {
171
+ * await Koolbase.storage.updateMetadata('photos', 'sunset.jpg', {
172
+ * tag: 'sunset',
173
+ * 'BAD KEY': 'oops',
174
+ * });
175
+ * } catch (e) {
176
+ * if (e instanceof KoolbaseStorageMetadataInvalidError) {
177
+ * console.warn('Metadata rejected:', e.detail);
178
+ * // -> 'Metadata rejected: key "BAD KEY": must match [a-z0-9_]+'
179
+ * }
180
+ * }
181
+ */
182
+ class KoolbaseStorageMetadataInvalidError extends KoolbaseStorageError {
183
+ constructor(message, detail) {
184
+ super(message ?? 'Metadata payload is invalid', 'metadata_invalid');
185
+ this.detail = detail;
186
+ this.name = 'KoolbaseStorageMetadataInvalidError';
187
+ Object.setPrototypeOf(this, KoolbaseStorageMetadataInvalidError.prototype);
188
+ }
189
+ }
190
+ exports.KoolbaseStorageMetadataInvalidError = KoolbaseStorageMetadataInvalidError;
151
191
  /**
152
192
  * Maps a non-2xx storage-layer response to a typed
153
193
  * {@link KoolbaseStorageError}, preferring the server's stable `code` and
@@ -173,6 +213,8 @@ function koolbaseStorageError(status, body, fallbackMessage = 'Storage request f
173
213
  return new KoolbaseStorageFileTooLargeError(message);
174
214
  case 'mime_not_allowed':
175
215
  return new KoolbaseStorageMimeTypeError(message);
216
+ case 'metadata_invalid':
217
+ return new KoolbaseStorageMetadataInvalidError(message, body?.detail);
176
218
  }
177
219
  // ─── status fallback (pre-code servers or uncoded paths) ───
178
220
  switch (status) {
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { KoolbaseConfig, UploadOptions, UploadResult } from './types';
1
+ import { KoolbaseConfig, UploadOptions, UploadResult, KoolbaseObject } from './types';
2
2
  /**
3
3
  * Koolbase storage client — uploads, downloads, and deletes via presigned
4
4
  * Cloudflare R2 URLs.
@@ -23,15 +23,90 @@ export declare class KoolbaseStorage {
23
23
  * Set `overwrite: true` for true upsert semantics — silently replace any
24
24
  * existing object at this path.
25
25
  *
26
+ * Pass `options.metadata` to attach arbitrary user-defined key/value pairs
27
+ * to the object at confirm time. Subject to the limits documented on
28
+ * {@link KoolbaseObject.metadata}; violations throw
29
+ * `KoolbaseStorageMetadataInvalidError`. On the `overwrite: true` path the
30
+ * metadata REPLACES any prior metadata at this path (matches GCS semantics).
31
+ * Use {@link updateMetadata} for post-upload merge changes.
32
+ *
26
33
  * **Breaking change in v5.0.0**: the default flipped from silent overwrite
27
34
  * (legacy behavior) to safe-by-default. If you previously relied on uploads
28
35
  * overwriting silently, pass `overwrite: true` explicitly.
29
36
  */
30
37
  upload(options: UploadOptions): Promise<UploadResult>;
38
+ /**
39
+ * Apply a partial metadata update to an existing object. Returns the
40
+ * post-update {@link KoolbaseObject} with the merged metadata.
41
+ *
42
+ * **Merge semantics** (mirrors the server's JSONB merge):
43
+ *
44
+ * - Keys with a non-null string value are SET — added if missing,
45
+ * replacing any existing value at the key otherwise.
46
+ * - Keys with `null` are DELETED from the stored metadata.
47
+ * - Keys ABSENT from `metadata` are untouched — pre-existing entries
48
+ * for those keys remain unchanged.
49
+ *
50
+ * Validation runs server-side against the same rules as upload-time
51
+ * metadata; violations throw `KoolbaseStorageMetadataInvalidError`,
52
+ * whose `detail` field names the failing key and rule. The check is
53
+ * performed against the projected post-merge state, so adding a key
54
+ * that would push the object past the 50-key or 8KB ceiling is
55
+ * rejected before the row is mutated.
56
+ *
57
+ * @example
58
+ * // Add a tag, update an existing key, and drop another in one call:
59
+ * const updated = await Koolbase.storage.updateMetadata(
60
+ * 'photos',
61
+ * 'sunset.jpg',
62
+ * {
63
+ * category: 'landscape', // SET or UPDATE
64
+ * tag: 'sunset', // SET or UPDATE
65
+ * owner: null, // DELETE
66
+ * }
67
+ * );
68
+ * console.log(updated.metadata);
69
+ * // -> { category: 'landscape', tag: 'sunset' }
70
+ */
71
+ updateMetadata(bucket: string, path: string, metadata: Record<string, string | null>): Promise<KoolbaseObject>;
31
72
  /**
32
73
  * Get a signed download URL for a file.
33
74
  */
34
75
  getDownloadUrl(bucket: string, path: string): Promise<string>;
76
+ /**
77
+ * Build the stable public CDN URL for a file in a public bucket.
78
+ *
79
+ * Returns the URL unconditionally — no check on whether the file
80
+ * exists or whether the bucket is actually public. Use when you
81
+ * know the file is in a public bucket and want the URL without a
82
+ * network round-trip (build-time URL generation, server-side
83
+ * rendering, batch image processing, etc.).
84
+ *
85
+ * For safer construction from an Object you already have, use
86
+ * {@link KoolbaseStorage.publicUrlForObject} — it checks the stored
87
+ * `r2Bucket` value and returns `null` when the object isn't in the
88
+ * public R2 bucket.
89
+ */
90
+ static publicUrl(args: {
91
+ projectId: string;
92
+ bucket: string;
93
+ path: string;
94
+ }): string;
95
+ /**
96
+ * Returns the stable CDN URL for an object when its bytes physically
97
+ * live in the public R2 bucket, `null` otherwise.
98
+ *
99
+ * Returns `null` for:
100
+ * - Files in private buckets (no public URL ever)
101
+ * - Legacy files in public buckets whose bytes still live in the
102
+ * private R2 bucket from before Gap #2 (no permanent URL until
103
+ * they're re-uploaded)
104
+ *
105
+ * The bucket name must be supplied because {@link KoolbaseObject}
106
+ * carries only the bucket ID, not its name. Typically the caller
107
+ * already knows which bucket they queried.
108
+ */
109
+ static publicUrlForObject(obj: KoolbaseObject, bucket: string): string | null;
35
110
  /**
36
111
  * Delete a file from a bucket.
37
112
  */
package/dist/storage.js CHANGED
@@ -33,6 +33,13 @@ class KoolbaseStorage {
33
33
  * Set `overwrite: true` for true upsert semantics — silently replace any
34
34
  * existing object at this path.
35
35
  *
36
+ * Pass `options.metadata` to attach arbitrary user-defined key/value pairs
37
+ * to the object at confirm time. Subject to the limits documented on
38
+ * {@link KoolbaseObject.metadata}; violations throw
39
+ * `KoolbaseStorageMetadataInvalidError`. On the `overwrite: true` path the
40
+ * metadata REPLACES any prior metadata at this path (matches GCS semantics).
41
+ * Use {@link updateMetadata} for post-upload merge changes.
42
+ *
36
43
  * **Breaking change in v5.0.0**: the default flipped from silent overwrite
37
44
  * (legacy behavior) to safe-by-default. If you previously relied on uploads
38
45
  * overwriting silently, pass `overwrite: true` explicitly.
@@ -76,20 +83,28 @@ class KoolbaseStorage {
76
83
  }
77
84
  const etag = uploadRes.headers.get('etag') ?? '';
78
85
  // ─── Step 3: Confirm upload ───
86
+ // Build the body conditionally so the `metadata` field is only sent
87
+ // when the caller passed it — keeps the wire shape clean for callers
88
+ // that don't care, and lets the server's omitempty path treat absent
89
+ // as "no metadata."
90
+ const confirmBody = {
91
+ bucket: options.bucket,
92
+ path: options.path,
93
+ size: fileSize,
94
+ content_type: contentType,
95
+ etag,
96
+ overwrite,
97
+ };
98
+ if (options.metadata !== undefined) {
99
+ confirmBody.metadata = options.metadata;
100
+ }
79
101
  const confirmRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
80
102
  method: 'POST',
81
103
  headers: {
82
104
  ...(await this.buildHeaders()),
83
105
  'Content-Type': 'application/json',
84
106
  },
85
- body: JSON.stringify({
86
- bucket: options.bucket,
87
- path: options.path,
88
- size: fileSize,
89
- content_type: contentType,
90
- etag,
91
- overwrite,
92
- }),
107
+ body: JSON.stringify(confirmBody),
93
108
  });
94
109
  if (!confirmRes.ok) {
95
110
  throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(confirmRes, 'Failed to confirm upload');
@@ -100,6 +115,54 @@ class KoolbaseStorage {
100
115
  const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
101
116
  return { object, downloadUrl };
102
117
  }
118
+ /**
119
+ * Apply a partial metadata update to an existing object. Returns the
120
+ * post-update {@link KoolbaseObject} with the merged metadata.
121
+ *
122
+ * **Merge semantics** (mirrors the server's JSONB merge):
123
+ *
124
+ * - Keys with a non-null string value are SET — added if missing,
125
+ * replacing any existing value at the key otherwise.
126
+ * - Keys with `null` are DELETED from the stored metadata.
127
+ * - Keys ABSENT from `metadata` are untouched — pre-existing entries
128
+ * for those keys remain unchanged.
129
+ *
130
+ * Validation runs server-side against the same rules as upload-time
131
+ * metadata; violations throw `KoolbaseStorageMetadataInvalidError`,
132
+ * whose `detail` field names the failing key and rule. The check is
133
+ * performed against the projected post-merge state, so adding a key
134
+ * that would push the object past the 50-key or 8KB ceiling is
135
+ * rejected before the row is mutated.
136
+ *
137
+ * @example
138
+ * // Add a tag, update an existing key, and drop another in one call:
139
+ * const updated = await Koolbase.storage.updateMetadata(
140
+ * 'photos',
141
+ * 'sunset.jpg',
142
+ * {
143
+ * category: 'landscape', // SET or UPDATE
144
+ * tag: 'sunset', // SET or UPDATE
145
+ * owner: null, // DELETE
146
+ * }
147
+ * );
148
+ * console.log(updated.metadata);
149
+ * // -> { category: 'landscape', tag: 'sunset' }
150
+ */
151
+ async updateMetadata(bucket, path, metadata) {
152
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
153
+ method: 'PATCH',
154
+ headers: {
155
+ ...(await this.buildHeaders()),
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify({ bucket, path, metadata }),
159
+ });
160
+ if (!res.ok) {
161
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to update metadata');
162
+ }
163
+ const raw = await res.json();
164
+ return mapObjectFromServer(raw);
165
+ }
103
166
  /**
104
167
  * Get a signed download URL for a file.
105
168
  */
@@ -113,6 +176,45 @@ class KoolbaseStorage {
113
176
  const data = (await res.json());
114
177
  return data.url;
115
178
  }
179
+ /**
180
+ * Build the stable public CDN URL for a file in a public bucket.
181
+ *
182
+ * Returns the URL unconditionally — no check on whether the file
183
+ * exists or whether the bucket is actually public. Use when you
184
+ * know the file is in a public bucket and want the URL without a
185
+ * network round-trip (build-time URL generation, server-side
186
+ * rendering, batch image processing, etc.).
187
+ *
188
+ * For safer construction from an Object you already have, use
189
+ * {@link KoolbaseStorage.publicUrlForObject} — it checks the stored
190
+ * `r2Bucket` value and returns `null` when the object isn't in the
191
+ * public R2 bucket.
192
+ */
193
+ static publicUrl(args) {
194
+ // Encode each path segment individually so slashes are preserved
195
+ // while spaces, parens, hashes, and query characters are escaped.
196
+ const encoded = args.path.split('/').map(encodeURIComponent).join('/');
197
+ return `https://cdn.koolbase.com/${args.projectId}/${args.bucket}/${encoded}`;
198
+ }
199
+ /**
200
+ * Returns the stable CDN URL for an object when its bytes physically
201
+ * live in the public R2 bucket, `null` otherwise.
202
+ *
203
+ * Returns `null` for:
204
+ * - Files in private buckets (no public URL ever)
205
+ * - Legacy files in public buckets whose bytes still live in the
206
+ * private R2 bucket from before Gap #2 (no permanent URL until
207
+ * they're re-uploaded)
208
+ *
209
+ * The bucket name must be supplied because {@link KoolbaseObject}
210
+ * carries only the bucket ID, not its name. Typically the caller
211
+ * already knows which bucket they queried.
212
+ */
213
+ static publicUrlForObject(obj, bucket) {
214
+ if (obj.r2Bucket !== 'koolbase-storage-public')
215
+ return null;
216
+ return KoolbaseStorage.publicUrl({ projectId: obj.projectId, bucket, path: obj.path });
217
+ }
116
218
  /**
117
219
  * Delete a file from a bucket.
118
220
  */
@@ -135,6 +237,9 @@ class KoolbaseStorage {
135
237
  exports.KoolbaseStorage = KoolbaseStorage;
136
238
  /**
137
239
  * Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
240
+ * Defensive: missing or null `metadata` (older / non-Koolbase responses)
241
+ * is coerced to an empty object so callers always see a typed
242
+ * `Record<string, string>` rather than null.
138
243
  */
139
244
  function mapObjectFromServer(raw) {
140
245
  return {
@@ -145,6 +250,8 @@ function mapObjectFromServer(raw) {
145
250
  path: raw.path,
146
251
  size: raw.size ?? 0,
147
252
  contentType: raw.content_type ?? null,
253
+ metadata: raw.metadata ?? {},
254
+ r2Bucket: raw.r2_bucket ?? 'koolbase-storage',
148
255
  createdAt: raw.created_at,
149
256
  updatedAt: raw.updated_at,
150
257
  };
package/dist/types.d.ts CHANGED
@@ -161,6 +161,20 @@ export interface UploadOptions {
161
161
  * `true` to silently replace the existing object.
162
162
  */
163
163
  overwrite?: boolean;
164
+ /**
165
+ * User-defined key/value metadata to attach to the object at confirm
166
+ * time. Optional — when omitted, the object stores empty metadata `{}`.
167
+ *
168
+ * Subject to server-side validation (≤50 keys, ≤8KB total, keys 1–64
169
+ * chars matching `[a-z0-9_]+`, values ≤1024 chars, leading underscore
170
+ * reserved); violations throw `KoolbaseStorageMetadataInvalidError`.
171
+ *
172
+ * On the `overwrite: true` path, metadata REPLACES any prior metadata
173
+ * at this path (matches GCS semantics — a new upload at a path
174
+ * produces a new object, not a patch of the old). Use `updateMetadata`
175
+ * for post-upload merge changes.
176
+ */
177
+ metadata?: Record<string, string>;
164
178
  onProgress?: (percent: number) => void;
165
179
  }
166
180
  /**
@@ -171,10 +185,28 @@ export interface KoolbaseObject {
171
185
  id: string;
172
186
  projectId: string;
173
187
  bucketId: string;
188
+ /**
189
+ * Name of the physical R2 bucket holding this object's bytes
190
+ * (Gap #2). `'koolbase-storage-public'` means the object has a
191
+ * stable CDN URL — construct it with
192
+ * `KoolbaseStorage.publicUrlForObject(obj, bucket)`. Anything else
193
+ * (typically `'koolbase-storage'`) means the object is in private
194
+ * storage and reads go through {@link KoolbaseStorage.getDownloadUrl},
195
+ * which returns a 1-hour presigned URL.
196
+ */
197
+ r2Bucket: string;
174
198
  userId: string | null;
175
199
  path: string;
176
200
  size: number;
177
201
  contentType: string | null;
202
+ /**
203
+ * User-defined key/value metadata attached to this object. Always
204
+ * non-null — empty object when no metadata has been set (the server
205
+ * returns `{}` rather than `null` so callers can treat it as a
206
+ * guaranteed object without null checks). Set on upload via
207
+ * `upload({ metadata })` or mutated post-upload via `updateMetadata`.
208
+ */
209
+ metadata: Record<string, string>;
178
210
  /** ISO 8601 timestamp from the server. */
179
211
  createdAt: string;
180
212
  /** ISO 8601 timestamp from the server. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "5.1.1",
3
+ "version": "5.3.0",
4
4
  "description": "React Native SDK for Koolbase — auth, database, storage, realtime, feature flags, and functions in one package.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,8 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/jszip": "^3.4.0",
30
+ "@types/node": "^25.9.1",
31
+ "react-native": "^0.85.3",
30
32
  "react-native-keychain": "^10.0.0",
31
33
  "typescript": "^5.0.0"
32
34
  },