@koolbase/react-native 9.2.0 → 10.0.1

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +1348 -0
  2. package/README.md +403 -568
  3. package/dist/{auth-storage.d.ts → cjs/auth-storage.d.ts} +1 -1
  4. package/dist/cjs/index.d.ts +19 -0
  5. package/dist/cjs/index.js +125 -0
  6. package/dist/cjs/package.json +3 -0
  7. package/dist/cjs/platform.d.ts +2 -0
  8. package/dist/cjs/platform.js +43 -0
  9. package/dist/esm/auth-storage.d.ts +26 -0
  10. package/dist/esm/auth-storage.js +100 -0
  11. package/dist/esm/index.d.ts +19 -0
  12. package/dist/esm/index.js +106 -0
  13. package/dist/esm/package.json +3 -0
  14. package/dist/esm/platform.d.ts +2 -0
  15. package/dist/esm/platform.js +37 -0
  16. package/package.json +30 -30
  17. package/dist/analytics.d.ts +0 -24
  18. package/dist/analytics.js +0 -114
  19. package/dist/apple-auth.d.ts +0 -22
  20. package/dist/apple-auth.js +0 -74
  21. package/dist/auth-errors.d.ts +0 -117
  22. package/dist/auth-errors.js +0 -250
  23. package/dist/auth.d.ts +0 -213
  24. package/dist/auth.js +0 -810
  25. package/dist/cache-store.d.ts +0 -50
  26. package/dist/cache-store.js +0 -197
  27. package/dist/code-push.d.ts +0 -59
  28. package/dist/code-push.js +0 -255
  29. package/dist/conflict.d.ts +0 -80
  30. package/dist/conflict.js +0 -84
  31. package/dist/database-errors.d.ts +0 -101
  32. package/dist/database-errors.js +0 -200
  33. package/dist/database.d.ts +0 -298
  34. package/dist/database.js +0 -852
  35. package/dist/device-id.d.ts +0 -1
  36. package/dist/device-id.js +0 -60
  37. package/dist/device-metadata.d.ts +0 -36
  38. package/dist/device-metadata.js +0 -102
  39. package/dist/errors.d.ts +0 -64
  40. package/dist/errors.js +0 -85
  41. package/dist/flags.d.ts +0 -15
  42. package/dist/flags.js +0 -76
  43. package/dist/function-errors.d.ts +0 -51
  44. package/dist/function-errors.js +0 -103
  45. package/dist/functions.d.ts +0 -15
  46. package/dist/functions.js +0 -83
  47. package/dist/index.d.ts +0 -49
  48. package/dist/index.js +0 -204
  49. package/dist/logic-engine.d.ts +0 -17
  50. package/dist/logic-engine.js +0 -193
  51. package/dist/messaging.d.ts +0 -13
  52. package/dist/messaging.js +0 -36
  53. package/dist/offline-state.d.ts +0 -97
  54. package/dist/offline-state.js +0 -200
  55. package/dist/pending-write.d.ts +0 -47
  56. package/dist/pending-write.js +0 -22
  57. package/dist/realtime.d.ts +0 -44
  58. package/dist/realtime.js +0 -195
  59. package/dist/record.d.ts +0 -2
  60. package/dist/record.js +0 -23
  61. package/dist/storage-errors.d.ts +0 -163
  62. package/dist/storage-errors.js +0 -253
  63. package/dist/storage.d.ts +0 -198
  64. package/dist/storage.js +0 -451
  65. package/dist/sync-engine.d.ts +0 -30
  66. package/dist/sync-engine.js +0 -290
  67. package/dist/types.d.ts +0 -487
  68. package/dist/types.js +0 -40
  69. /package/dist/{auth-storage.js → cjs/auth-storage.js} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,1348 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@koolbase/react-native` are documented
4
+ in this file. The format is based on [Keep a Changelog][kac], and this project
5
+ adheres to [Semantic Versioning][semver].
6
+
7
+ [kac]: https://keepachangelog.com/en/1.1.0/
8
+ [semver]: https://semver.org/
9
+
10
+ ## 10.0.1
11
+
12
+ No changes to this package. Released to keep the version aligned with
13
+ `@koolbase/core` and `@koolbase/js`, which carry a fix for running outside a
14
+ browser.
15
+
16
+ ## 10.0.0
17
+
18
+ ### Read before upgrading
19
+
20
+ **The package is now a thin layer over `@koolbase/core`.** The SDK's behaviour
21
+ — auth, database, offline queue, conflicts, realtime, storage, functions —
22
+ moved into a shared core that `@koolbase/react-native` and the new
23
+ `@koolbase/js` both compose. Your imports do not change: everything the core
24
+ exports is re-exported here. What changes is that the same sixty tests that
25
+ proved the offline path on a device now run against both hosts, so a fix in
26
+ one is a fix in the other.
27
+
28
+ **Code push is gone.** `Koolbase.codePush`, `codePushChannel`,
29
+ `onMandatoryUpdate`, `KoolbaseCodePush`, `BundleManifest` and `BundlePayload`
30
+ are removed. Code push is a Flutter feature, where it patches the Dart VM; the
31
+ React Native version pushed config and flag overrides, which Remote Config and
32
+ Feature Flags already do without a bundle. If you set `codePushChannel`, delete
33
+ the line; the flag and config reads keep working from the remote values.
34
+
35
+ **`Koolbase.executeFlow` and the logic engine are gone.** Flows only ever
36
+ arrived through code-push bundles, so with no bundle the method could only
37
+ return an empty result. Removed rather than left as a call that does nothing.
38
+
39
+ **`KoolbaseAppleAuth` is gone.** Deprecated since 1.9.0, throwing
40
+ `not_implemented` since then. `Koolbase.auth.signInWithApple(...)` is the
41
+ Apple flow and is unchanged.
42
+
43
+ ### Changed
44
+
45
+ - **Host access goes through a platform adapter.** AsyncStorage, NetInfo,
46
+ AppState and Platform are consumed by one file, `platform.ts`, and nothing
47
+ else in the SDK imports a native module. `KoolbaseConfig.platform` accepts
48
+ a custom adapter — for tests, or for a host this package does not cover.
49
+
50
+ - **The default session storage is decided by the platform.** Keychain when
51
+ `react-native-keychain` is installed, as before; the choice is now made by
52
+ the adapter rather than hard-coded in auth.
53
+
54
+ - **The device label persists through the platform's storage** rather than a
55
+ direct keychain call. Behaviour is the same on a device with keychain; on
56
+ one without, the label now survives restarts where it used to be regenerated
57
+ each session.
58
+
59
+ ### Removed
60
+
61
+ - `jszip` — code push was its only user. This package's core now has no
62
+ runtime dependencies.
63
+
64
+ ### Migration
65
+
66
+ Delete any `codePushChannel` or `onMandatoryUpdate` from your `initialize()`
67
+ config, and any `Koolbase.codePush` or `Koolbase.executeFlow` calls. That is
68
+ the whole migration; everything else is source-compatible.
69
+
70
+ ## 9.2.0
71
+
72
+ ### Read before upgrading
73
+
74
+ **`delete()` can now fail.** It previously returned `Promise<void>` with no throw
75
+ path: a delete the server refused — no permission, wrong project, record already
76
+ gone — reported success to the caller, and nothing anywhere reported otherwise.
77
+ It now throws. Code that called it without a `catch` will surface an error where
78
+ it never did before, which is the point, and still a change.
79
+
80
+ **A rejected credential is no longer a `KoolbaseDataError`.** It is
81
+ `KoolbaseUnauthenticatedError`, a sibling under the new shared root. Code
82
+ catching `KoolbaseDataError` to handle a dead session will stop matching.
83
+
84
+ ### Fixed
85
+
86
+ - **`delete()` queued the write before attempting it, and never removed it.** A
87
+ delete that succeeded stayed in the queue and replayed later — against a
88
+ record that may since have been recreated under the same id.
89
+
90
+ - **Five data methods could not clear a rejected session.** `upsert`,
91
+ `deleteWhere`, `batch`, `setVector`, and `deleteVector` each built their own
92
+ request, so whether a 401 signed you out depended on which method you called.
93
+ All now go through one path.
94
+
95
+ - **A 401 was treated as an unreachable network** by `update`, `delete`, and
96
+ `upsert`, so the write was queued and an optimistic record returned — telling
97
+ the app the change had succeeded. Anything the server answered with is now
98
+ surfaced, because it will be refused again on every retry.
99
+
100
+ - **A 401 carrying a body `code` did not clear the session.** Responses were
101
+ mapped by their body's `code` field before their status, so a server answering
102
+ a rejected credential with `code: 'validation_error'` produced a data error and
103
+ left the dead session in place — the app kept making calls that could only
104
+ fail. Status is now authoritative: a 401 is an authentication failure whatever
105
+ the body says.
106
+
107
+ - **Offline insert chains broke at the server boundary.** A record created
108
+ offline got a `local_` id, and any queued update or delete addressed that id —
109
+ which ceased to exist the moment the insert replayed and the server assigned a
110
+ real one. The follow-up writes failed against a record that was sitting right
111
+ there. Ids are now UUID v4 from the moment of creation, client-side, and the
112
+ id travels with the queued insert, so a chain of edits made offline replays
113
+ exactly as it was made.
114
+
115
+ - **Every cold `query()` hit the network twice.** The stale-while-revalidate
116
+ refresh was fired before the cache was consulted, so a query with nothing
117
+ cached issued the background request and the real one. Doubled reads on every
118
+ first load, against your quota.
119
+
120
+ - **Signed-out state read as empty state.** Per-user caches and queues fell back
121
+ to a shared anonymous bucket when no user was present, so a signed-out call
122
+ reported zero pending writes rather than refusing — a sync indicator could show
123
+ "all synced" over a queue that was merely out of reach. Per-user surfaces now
124
+ refuse without a user.
125
+
126
+ - **A replayed write left the cache it invalidated behind.** Replay updated the
127
+ server and stopped there: a record deleted offline stayed in every cached query
128
+ result after its delete succeeded, and an updated one kept its pre-edit values
129
+ until something else evicted it. Replay now maintains the cache at the point of
130
+ success.
131
+
132
+ - **A terminally rejected insert left its optimistic record standing.** When the
133
+ server refused a queued insert for good — no permission, validation — the write
134
+ left the queue but the optimistic record stayed in every cached result it had
135
+ been written into. The app displayed a record that does not exist and never
136
+ will. Terminal rejection now evicts it.
137
+
138
+ - **Realtime reconnected every three seconds forever**, with no backoff and no
139
+ ceiling. A device with no network, a wrong URL, or a dead session drained
140
+ battery and data invisibly. It now doubles to a minute and resets when a
141
+ connection opens.
142
+
143
+ - **`clearUserCache` deleted the write queue along with the cache.** Nothing
144
+ called it, which is the only reason it had not lost anyone's work.
145
+
146
+ ### Added
147
+
148
+ - **One exception hierarchy.** `KoolbaseError` is the root; the data, storage,
149
+ auth, and Function families sit beneath it, so `catch (e) { if (e instanceof
150
+ KoolbaseError) }` covers any SDK failure.
151
+
152
+ - **`KoolbaseUnauthenticatedError`** — raised by any surface. A session stops
153
+ working for the whole SDK at once, not one subsystem at a time. Named for what
154
+ the server reports: a 401 covers an expired session, a revoked key, and
155
+ missing credentials, and it does not distinguish them.
156
+
157
+ - **`auth.clearStoredSession()`** — discards a session already known to be
158
+ unusable, without a server call.
159
+
160
+ - **Typed Function failures.** `FunctionNotFoundError`,
161
+ `FunctionPermissionError`, `FunctionValidationError`,
162
+ `FunctionQuotaExceededError`, `FunctionExecutionError`. Every failed
163
+ invocation used to be a bare `Error`, matchable only on message text.
164
+
165
+ - The package's first tests: 60, covering the paths above.
166
+
167
+ ### Added — the queue is observable
168
+
169
+ - **`db.pendingWrites()`** — changes made offline, waiting to be sent, oldest
170
+ first. For sync indicators and for warning a user about to log out with
171
+ unsynced edits: queues are per-user and survive logout by design, so those
172
+ edits sync whenever that user next signs in on this device — possibly never.
173
+ `conflicts()` got this treatment; the queue, the same durable state one step
174
+ earlier, now has it too.
175
+ - The returned shape deliberately excludes replay internals (baselines,
176
+ revisions). What is public is what an app needs to display.
177
+ - **Renamed:** the old exported `PendingWrite` interface — the 9.1.x queue-entry
178
+ shape, which no API ever returned — is no longer public. The name now refers
179
+ to the observable queue entry above. The old shape survives internally only
180
+ for the legacy-queue migration.
181
+
182
+ ### Added — offline editing that cannot overwrite silently
183
+
184
+ Offline `update` and `delete` used to be queued without recording what the change
185
+ was based on, so replay applied them blindly and overwrote anything that had
186
+ changed meanwhile. Now:
187
+
188
+ - A write is queued only when the SDK knows what the record looked like at the
189
+ time — from a query, a read, a realtime event, or a still-queued insert.
190
+ Otherwise it throws `KoolbaseOfflineBaselineUnavailableError` rather than
191
+ queueing something that cannot be replayed safely.
192
+
193
+ - Replay sends the revision the change was based on, so the server applies it
194
+ only if the record still carries that revision. Nothing can land between the
195
+ client deciding a write is safe and the server applying it.
196
+
197
+ - A refused write becomes a durable conflict rather than a retry, readable from
198
+ `Koolbase.db.conflicts()` and resolvable four ways. Conflicts survive
199
+ restarts, and do not expire: an app that never reads them accumulates them
200
+ invisibly, so surface them if you support offline editing.
201
+
202
+ - A refused insert is now a conflict you can resolve. It was recorded as an
203
+ `update`, and resolution had no branch for an insert at all, so the one class
204
+ of conflict that loses a record outright was the one class you could not act
205
+ on. A rejected insert now holds its operation, and resolving it retries the
206
+ insert, carrying the conflict id as the idempotency key so a retry cannot
207
+ double-write.
208
+
209
+ - A resolution the server refuses no longer disappears. The conflict is updated
210
+ with what the server returned and stands, rather than being cleared on the
211
+ assumption the resolution landed.
212
+
213
+ - Writes queued by an earlier version are migrated on first sync. Inserts replay
214
+ normally; updates and deletes have no baseline, so they are preserved as
215
+ conflicts marked `baseline_unavailable` rather than replayed blindly or
216
+ dropped. The migration never touches the network, so its outcome does not
217
+ depend on whether the device happened to be online at startup.
218
+
219
+ - Records reach a per-record cache from every path that returns one — queries,
220
+ reads, writes, batch results, search hits, and realtime events — so anything
221
+ the SDK has fully seen can be edited offline.
222
+
223
+ ## 9.1.0
224
+
225
+ ### Fixed
226
+
227
+ - **Device identity is now a single shared, persisted value across the SDK.**
228
+ Feature flags, code push, and messaging were each initialized with a
229
+ hardcoded `'rn-device'` string instead of a real device id. This silently
230
+ broke three things: every device collided on one messaging registration row
231
+ (only the last-registered device per project received push); every device
232
+ produced the same feature-flag rollout bucket (a 10% rollout was on for
233
+ everyone or no one, never 10%); and code-push targeting collided the same
234
+ way. A single `getOrCreateDeviceId()` now generates a persisted UUID v4 once
235
+ (crypto-backed where the runtime provides it) and all subsystems share it.
236
+
237
+ ### Removed
238
+
239
+ - **`Koolbase.messaging.send()` and `SendOptions`.** Sending push notifications
240
+ is server-initiated only — it requires a secret `kb_live_` key and must run
241
+ on your backend or in a Koolbase Function, never in the app. The publishable
242
+ key the SDK holds ships in your bundle; a client that could send would let
243
+ anyone extracting it push to your users. The API already rejected
244
+ publishable-key sends with 401, so this method never delivered. Move sends to
245
+ your backend. `registerToken` is unchanged.
246
+
247
+ ### Note
248
+
249
+ - On upgrade, devices are assigned a proper unique id and will re-register
250
+ with messaging once. No action needed. Feature-flag rollout buckets will
251
+ change (correctly) — a device that happened to fall in or out of a rollout
252
+ under the old constant behaviour may now flip, matching its true bucket.
253
+
254
+ ## 9.0.0
255
+
256
+ ### Breaking changes
257
+
258
+ - **Package renamed** from `@techfinityedge/koolbase-react-native` to `@koolbase/react-native` for brand consistency with the rest of the Koolbase SDKs and tooling. This is the only change in this release — the API surface, behavior, and exports are identical to 8.0.0.
259
+ - **Migration:** replace the dependency in `package.json` (`@techfinityedge/koolbase-react-native` → `@koolbase/react-native`) and update every import path accordingly. No code changes beyond the import specifier are required. The old package is deprecated on npm and will receive no further updates.
260
+
261
+ ## 8.0.0
262
+
263
+ ### Breaking changes
264
+
265
+ - None. `mode` and `minSimilarity` are both optional; existing
266
+ `searchSemantic` callers continue to work unchanged. Major bump
267
+ reflects the conceptual expansion of the search contract (three
268
+ retrieval modes instead of one), not API-breaking removals.
269
+
270
+ ### Added
271
+
272
+ - `KoolbaseDatabase.searchSemantic` accepts a new `mode` parameter of
273
+ type `SearchMode`. Three retrieval strategies are supported:
274
+ - `'semantic'` (default) — pure vector search via HNSW on cosine
275
+ distance. Best for fuzzy / conceptual queries.
276
+ - `'lexical'` — pure BM25 over the field's source text via Postgres
277
+ `ts_rank_cd`. Best for exact terms, codes, names, acronyms.
278
+ - `'hybrid'` — vector + lexical fused with reciprocal rank fusion
279
+ (k=60). Generally the strongest default for production search.
280
+ - `KoolbaseDatabase.searchSemantic` accepts a new `minSimilarity`
281
+ parameter (0..100, optional). Server-side filter that drops results
282
+ below the given similarity percentage before they cross the wire.
283
+ Saves bandwidth on weak matches. Only valid for semantic and hybrid
284
+ modes; the server rejects it on lexical mode (BM25 ranks aren't
285
+ comparable to cosine similarity).
286
+ - New `SearchMode` type exported from `@koolbase/react-native`.
287
+
288
+ ### Server requirements
289
+
290
+ - Requires Koolbase API release with hybrid search shipped (June 8 2026
291
+ or later).
292
+ - Lexical and hybrid modes require the vector field to have a
293
+ `source_field` configured. The lexical sidecar table populates
294
+ automatically on record write via the same hook that drives auto-embed.
295
+
296
+ ## 7.0.0
297
+
298
+ ### Breaking changes
299
+
300
+ - `KoolbaseDatabase.searchSemantic`: the `queryVector` parameter is now optional.
301
+ Existing callers continue to work unchanged — the breaking aspect is that
302
+ the SDK now validates that exactly one of `queryVector` / `queryText` is
303
+ supplied, and throws `Error` otherwise.
304
+
305
+ ### Added
306
+
307
+ - `KoolbaseDatabase.searchSemantic` accepts a new `queryText` parameter. When
308
+ supplied, the server embeds it inline using the vector field's configured
309
+ provider (Gemini or OpenAI) before running HNSW lookup. No client-side
310
+ embedding model required for typical search use cases.
311
+ - `KoolbaseDatabase.embedText` queues an embedding job for a specific record's
312
+ vector field. Used for backfilling vectors on records that pre-date the
313
+ auto-embed hook, or for embedding text other than the record's
314
+ configured source field.
315
+
316
+ ### Server requirements
317
+
318
+ - Requires Koolbase API release `771728d` or later (Phase 2 Stage A3a).
319
+ - Auto-embed on record write is automatic once a vector field has its
320
+ `embedding_provider`, `embedding_model`, and `source_field` configured
321
+ (see [docs](https://docs.koolbase.com/database/vectors)).
322
+
323
+ ## 6.0.0
324
+
325
+ ### Added — database
326
+
327
+ - **Semantic search via vector similarity.** Query records by meaning,
328
+ not just by field equality. Companion to the server-side vector
329
+ primitive shipped in Koolbase Phase 1 AI on June 6 2026.
330
+ - `KoolbaseDatabase.setVector(recordId, field, vector)` writes (or
331
+ replaces) a vector for a record on the named field. The field must
332
+ already be declared on the collection via the dashboard or CLI;
333
+ the vector's length must match the field's declared dimension.
334
+ - `KoolbaseDatabase.getVector(recordId, field)` reads a stored vector
335
+ back as `KoolbaseVector` — `{ recordId, fieldName, vector,
336
+ createdAt, updatedAt }`.
337
+ - `KoolbaseDatabase.deleteVector(recordId, field)` removes a record's
338
+ vector slot. Does NOT remove the field declaration itself — the
339
+ field stays settable on other records.
340
+ - `KoolbaseDatabase.searchSemantic({ collection, field, queryVector,
341
+ limit, where })` runs an HNSW similarity search ranking records by
342
+ cosine distance to the query vector. The collection's read rule is
343
+ applied after the lookup; `where` is an optional equality filter
344
+ map. Returns `SemanticSearchResult` — `{ hits, total }` where each
345
+ hit carries `record` and `distance`.
346
+
347
+ - New typed exports: `KoolbaseVector`, `KoolbaseSemanticHit`,
348
+ `SemanticSearchResult`, `KoolbaseVectorDimensionMismatchError`.
349
+
350
+ ### Fixed — docs
351
+
352
+ - Database errors table in `README.md` previously listed storage error
353
+ rows (copy-paste bug). Now lists the actual database error subclasses.
354
+
355
+ ### Notes
356
+
357
+ - Vector fields must be declared ahead of time via the Koolbase
358
+ dashboard or CLI; the React Native SDK does not declare schema
359
+ (mirrors how collections and storage buckets are declared).
360
+ - Supported dimensions in this release: 384, 768, 1024, 1536. Higher
361
+ dimensions (e.g. OpenAI text-embedding-3-large at 3072) will be
362
+ supported in a future release once pgvector is upgraded — in the
363
+ meantime, use the model's `dimensions=1536` parameter (Matryoshka
364
+ truncation) for full compatibility.
365
+ - Vector operations are online-only. They're not cached locally or
366
+ queued offline because HNSW similarity search has no useful offline
367
+ semantics, so deferred writes could corrupt the user's view of what's
368
+ persisted.
369
+ - Semantic search respects the collection's read rule the same way
370
+ `query()` does — `owner`/`scoped`/`conditional` records are filtered
371
+ to the caller after the HNSW lookup, so strict rules may return fewer
372
+ than `limit` results.
373
+
374
+ ### Migration
375
+
376
+ Purely additive — no existing methods, types, or exports changed.
377
+ Upgrading from 5.x requires only `yarn upgrade
378
+ @koolbase/react-native` (or the equivalent npm/pnpm/bun
379
+ command) and rebuilding (`yarn build`).
380
+
381
+ ### Added — storage
382
+
383
+ - Object versioning (Gap #6). When a bucket has versioning enabled (via
384
+ the dashboard or the buckets PATCH endpoint), every overwrite preserves
385
+ the prior bytes as a history version and deletes become soft —
386
+ recoverable until force-purged.
387
+ - `KoolbaseStorage.listVersions(bucket, path)` returns the full timeline
388
+ newest-first. Each `KoolbaseObjectVersion` carries `versionId`, `size`,
389
+ `etag`, `metadata`, `createdAt`, plus the flags `isCurrent` (the row
390
+ that lives in `storage_objects` right now) and `isDeleteMarker` (a
391
+ soft-delete tombstone with no fetchable bytes). Returns an empty array
392
+ for a path with no current row and no history.
393
+ - `KoolbaseStorage.getVersion(bucket, path, versionId)` fetches metadata
394
+ for a single version. Works against the current row or any history
395
+ row — check `isCurrent` to disambiguate.
396
+ - `KoolbaseStorage.getDownloadUrl(bucket, path, versionId?)` now accepts
397
+ an optional `versionId` argument. Omit to download the current bytes;
398
+ pass a `versionId` to download that historical version directly.
399
+ Throws for delete markers (no bytes exist).
400
+ - `KoolbaseStorage.restoreVersion(bucket, path, versionId)` brings a
401
+ history version back as the new current. The previously-current row
402
+ is snapshotted into history first, so restore is itself a versioned
403
+ event you can undo. The restored row gets a freshly-minted
404
+ `versionId`; the target stays in history at its original id.
405
+ - `KoolbaseStorage.purgeVersion(bucket, path, versionId)` hard-removes
406
+ a single history row plus its `.versions/` R2 bytes (or just the row,
407
+ for delete markers). Refuses the current version.
408
+ - `KoolbaseStorage.delete(bucket, path, forcePurge?)` now accepts an
409
+ optional `forcePurge` argument. With `forcePurge: true` against a
410
+ versioned bucket, wipes the entire timeline for a path — every row,
411
+ every R2 key. The default (`false`) is the soft-delete behavior:
412
+ snapshots current to history and records a delete marker.
413
+
414
+ - New typed export: `KoolbaseObjectVersion`.
415
+
416
+ ### Notes
417
+
418
+ - Versioning is opt-in per bucket. Buckets created before the feature
419
+ shipped — and any bucket with versioning off — keep the legacy
420
+ hard-overwrite, hard-delete semantics. No behavioral change for
421
+ non-versioned buckets.
422
+ - Delete markers can appear in `listVersions` results. Filter
423
+ client-side (`v => !v.isDeleteMarker`) if your UI only wants
424
+ restorable versions.
425
+ - `restoreVersion` against the already-current version, or against a
426
+ delete marker, throws. `getDownloadUrl` with a delete marker's
427
+ `versionId` throws.
428
+ - Backwards-compatible: all new APIs are additive. Existing
429
+ `getDownloadUrl(bucket, path)` and `delete(bucket, path)` calls
430
+ produce identical wire requests as before.
431
+ - Pairs with `koolbase_flutter` v6.5.0 (published earlier today). Same
432
+ client surface (`listVersions` / `getVersion` / `getDownloadUrl` with
433
+ `versionId` / `restoreVersion` / `purgeVersion` / `delete` with
434
+ `forcePurge`), same semantics.
435
+
436
+ ## 5.4.0
437
+
438
+ ### Added — storage
439
+
440
+ - Edge image transforms (Gap #8). Two complementary forms:
441
+ - `KoolbaseStorage.publicUrl({ ..., transform })` accepts an
442
+ optional `KoolbaseImageTransform` object — width, height,
443
+ format, quality, fit, dpr, gravity. The resulting URL hits
444
+ Cloudflare's image pipeline at
445
+ `cdn.koolbase.com/cdn-cgi/image/<opts>/...` and serves a
446
+ resized, re-encoded copy of the source.
447
+ - `KoolbaseStorage.publicUrlWithPreset({ projectId, presetName,
448
+ bucket, path })` resolves a named preset stored server-side
449
+ (managed via the dashboard or REST API) at
450
+ `cdn.koolbase.com/p/{project_id}/{preset_name}/{bucket}/{path}`.
451
+ Edit the preset once on the server and every URL using it
452
+ updates as the edge cache rolls over.
453
+ - `KoolbaseStorage.publicUrlForObjectWithPreset(obj, bucket,
454
+ presetName)` — instance-style variant when you already have a
455
+ `KoolbaseObject` in hand.
456
+ - New typed exports: `KoolbaseImageTransform`, `KoolbaseImageFormat`
457
+ (`'auto' | 'webp' | 'avif' | 'jpeg' | 'png'`), `KoolbaseImageFit`
458
+ (`'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'`),
459
+ `KoolbaseImageGravity` (10 anchor positions). Out-of-range numeric
460
+ values clamp silently to Cloudflare's valid ranges (width/height
461
+ 1–2000, quality 1–100, dpr 1–3).
462
+
463
+ ### Notes
464
+
465
+ Cloudflare bills unique transformations per calendar month; every
466
+ Koolbase account includes 5,000 free. Transformed responses are
467
+ edge-cached for 4 hours.
468
+
469
+ ### Compatibility
470
+
471
+ No breaking changes. All new APIs are additive; existing `publicUrl`
472
+ calls without `transform` produce the exact same URL they did in 5.3.0.
473
+
474
+ ## 5.3.0
475
+
476
+ ### Added — storage
477
+
478
+ - Public bucket CDN URLs (Gap #2 SDK polish).
479
+ - `KoolbaseObject` gains an `r2Bucket: string` field identifying
480
+ which physical R2 bucket holds the object's bytes. Always
481
+ populated. `'koolbase-storage-public'` means the object has a
482
+ stable CDN URL; anything else (typically `'koolbase-storage'`)
483
+ means it's in private storage and reads go through a presigned
484
+ URL via `getDownloadUrl`.
485
+ - `KoolbaseStorage.publicUrl({ projectId, bucket, path })` — static
486
+ method that builds the CDN URL pattern unconditionally. Use for
487
+ build-time URL generation where you have the inputs but don't
488
+ need (or want) a check that the file is actually in a public
489
+ bucket.
490
+ - `KoolbaseStorage.publicUrlForObject(obj, bucket)` — static method
491
+ that returns the stable CDN URL when the object lives in the
492
+ public R2 bucket, `null` otherwise. Use this when you have a
493
+ `KoolbaseObject` instance and want a safe URL — returns `null`
494
+ rather than a URL that 404s for private or legacy public-bucket
495
+ files.
496
+
497
+ ### Internal
498
+
499
+ - Storage object JSON mapper extended to surface the server's
500
+ `r2_bucket` field as `r2Bucket` on the typed `KoolbaseObject`.
501
+ Defaults to `'koolbase-storage'` when the field is absent (older
502
+ cached responses, non-Koolbase JSON) so existing code keeps
503
+ decoding without crashes.
504
+
505
+ ### Compatibility
506
+
507
+ No breaking changes. `getDownloadUrl` already returns the CDN URL
508
+ for objects in public buckets since the server-side Gap #2 deploy on
509
+ Jun 2 2026 — this release just makes that URL constructible without
510
+ a network round-trip.
511
+
512
+ ## 5.2.0
513
+
514
+ ### Added — storage
515
+
516
+ - Custom object metadata. Attach arbitrary key/value pairs to stored
517
+ objects at upload time, mutate via merge semantics post-upload, read
518
+ alongside any `KoolbaseObject`.
519
+ - `KoolbaseStorage.upload({ metadata })` accepts an optional
520
+ `metadata: Record<string, string>` field on `UploadOptions`. Set
521
+ at confirm time; REPLACES prior metadata on the `overwrite: true`
522
+ path (matches GCS semantics — a new upload at a path produces a
523
+ new object, not a patch of the old).
524
+ - New `KoolbaseStorage.updateMetadata(bucket, path, metadata)`
525
+ method with merge semantics: keys with a non-null string value
526
+ are set/updated, keys with `null` are deleted, keys absent from
527
+ the payload are untouched. One call handles add, update, and
528
+ delete atomically.
529
+ - `KoolbaseObject` gains a `metadata: Record<string, string>` field.
530
+ Always non-null — empty object `{}` when no metadata is set —
531
+ so callers can treat it as a guaranteed record without null
532
+ checks. Defensive decode handles missing/null `metadata` field
533
+ gracefully so older cached responses don't crash the mapper.
534
+ - New `KoolbaseStorageMetadataInvalidError` (extends
535
+ `KoolbaseStorageError`) thrown for server-side validation
536
+ failures (HTTP 400, code `metadata_invalid`). Its `detail` field
537
+ names the failing key and rule (e.g. `key "bad key": must match
538
+ [a-z0-9_]+`, `exceeds 50 keys (got 53)`) so callers can surface
539
+ actionable errors without guessing what shape rule was violated.
540
+ - Mapper recognizes `metadata_invalid` and extracts `detail` from the
541
+ response body.
542
+
543
+ ### Notes
544
+
545
+ - Validation rules (enforced server-side): ≤50 keys per object, ≤8KB
546
+ total (sum of all key + value lengths), keys 1–64 chars matching
547
+ `[a-z0-9_]+`, values ≤1024 chars, leading underscore reserved for
548
+ system keys.
549
+ - Backwards-compatible: pure additive surface. v5.1.1 → v5.2.0. Existing
550
+ `upload()` callers without `metadata` continue working unchanged;
551
+ catching `KoolbaseStorageError` still catches the new metadata error.
552
+ - Pairs with `koolbase_flutter` v6.2.0 (published earlier today). Same
553
+ client surface (`upload({ metadata })`, `updateMetadata`), same error
554
+ type semantics, same merge contract.
555
+
556
+ ## 5.1.1
557
+
558
+ ### Fixed — storage
559
+
560
+ - Storage error mapper now switches on lowercase wire codes
561
+ (`path_conflict`, `quota_exceeded`, `file_too_large`, `mime_not_allowed`)
562
+ after the server normalized storage codes to lowercase snake_case.
563
+ Without this patch, v5.1.0 customers see generic `KoolbaseStorageError`
564
+ instead of the typed subclass for storage limit errors. No semantic
565
+ changes beyond the case match. Pairs with `koolbase_flutter` v6.1.1.
566
+
567
+ ## 5.1.0
568
+
569
+ ### Added — storage
570
+
571
+ - Three new typed errors covering the bucket-limit failure modes
572
+ introduced server-side in Storage #2. All extend
573
+ `KoolbaseStorageError`, so existing `instanceof KoolbaseStorageError`
574
+ catch-all blocks continue to work; check the specific type to branch
575
+ on the kind of limit hit.
576
+ - `KoolbaseStorageQuotaError` — 409 + `QUOTA_EXCEEDED`, thrown when
577
+ an upload would push the bucket past its `max_size_bytes` cap.
578
+ - `KoolbaseStorageFileTooLargeError` — 413 + `FILE_TOO_LARGE`, thrown
579
+ when a single file exceeds the bucket's `max_file_size_bytes` cap.
580
+ - `KoolbaseStorageMimeTypeError` — 415 + `MIME_NOT_ALLOWED`, thrown
581
+ when an upload's content-type isn't in the bucket's
582
+ `allowed_mime_types` allowlist (supports `type/*` wildcards).
583
+ - Mapper (`koolbaseStorageError` / `koolbaseStorageErrorFromResponse`)
584
+ recognizes the new codes via code-first lookup and the new HTTP
585
+ statuses (413, 415) via status fallback.
586
+
587
+ ### Notes
588
+
589
+ - Backwards-compatible: pure additive surface. v5.0.0 → v5.1.0.
590
+ - Status-fallback for 409 remains `KoolbaseStorageConflictError` (path
591
+ collisions are the more common case); modern servers always emit
592
+ `code`, so the ambiguity only affects very old API responses.
593
+ - Pairs with `koolbase_flutter` v6.1.0 (published earlier today). Same
594
+ three error types, same code-first mapper extension.
595
+
596
+ ## 5.0.0
597
+
598
+ ### Breaking — storage
599
+
600
+ - **Storage URLs realigned to current server contract.** The v3.0.0 security
601
+ audit updated auth headers but left storage calling the old pre-refactor
602
+ endpoints (`/v1/sdk/storage/{bucket}/upload`, `/download`, `/delete`),
603
+ which the server no longer routes. **Storage uploads have been
604
+ non-functional since v3.0.0.** v5.0.0 realigns to the current contract:
605
+ `/v1/sdk/storage/upload-url`, `/confirm`, `/download-url`, `/object`.
606
+ - **3-step upload flow.** `upload()` now does presign → R2 PUT (raw binary, not
607
+ multipart) → confirm, matching Koolbase Flutter SDK v6.0.0. Confirmation
608
+ records the object in `storage_objects`, populates `etag`/`size`, and
609
+ prevents the orphan reaper from deleting your file. Previous "uploads"
610
+ bypassed confirm entirely and would have been swept on the next reaper pass.
611
+ - **`upload()` return shape changed.** Returns `UploadResult { object, downloadUrl }`
612
+ instead of `{ url }`. `object` is the full `KoolbaseObject` metadata
613
+ (id, size, content type, timestamps, etc.).
614
+ - **Safe-by-default uploads.** `UploadOptions` now accepts an `overwrite?: boolean`
615
+ field, defaulting to `false`. Uploads to a path where an object already
616
+ exists are **rejected** with a new `KoolbaseStorageConflictError` instead
617
+ of silently overwriting. Pass `overwrite: true` to opt into the previous
618
+ replacing behavior.
619
+ - **Storage operations now throw typed `KoolbaseStorageError` subtypes**
620
+ instead of generic `Error` — catching `Error` still works, but catching
621
+ the specific subclasses (or the `KoolbaseStorageError` base) gives you
622
+ cleaner branching.
623
+
624
+ ### Added
625
+
626
+ - `KoolbaseStorageError` — base class for all storage failures, mirroring
627
+ the `KoolbaseDataError` pattern from the database layer.
628
+ - `KoolbaseStorageConflictError` (`code: PATH_CONFLICT`) — thrown when an
629
+ upload would replace an existing object and `overwrite: false`. Exposes
630
+ the colliding `path` from the server response.
631
+ - `KoolbaseStorageNotFoundError`, `KoolbaseStorageValidationError`,
632
+ `KoolbaseStoragePermissionError` — typed errors for the other storage
633
+ error classes (404, 400, 403). Storage operations now throw these
634
+ instead of a generic `Error`.
635
+ - `koolbaseStorageError(status, body)` and
636
+ `koolbaseStorageErrorFromResponse(res)` — code-first response-to-error
637
+ mappers, matching the `database-errors` module pattern.
638
+ - `KoolbaseObject` and `UploadResult` types in `types.ts` — full object
639
+ metadata is now part of the public surface.
640
+
641
+ ### Migration
642
+
643
+ **If your app uploads to deterministic paths** (e.g. `avatars/${userId}.png`)
644
+ **and relied on the upload silently replacing the previous file:**
645
+
646
+ ```typescript
647
+ // Before — silent overwrite
648
+ await Koolbase.storage.upload({
649
+ bucket: 'avatars',
650
+ path: 'me.png',
651
+ file: { uri, name, type: 'image/png' },
652
+ });
653
+
654
+ // After — explicit overwrite
655
+ await Koolbase.storage.upload({
656
+ bucket: 'avatars',
657
+ path: 'me.png',
658
+ file: { uri, name, type: 'image/png' },
659
+ overwrite: true,
660
+ });
661
+ ```
662
+
663
+ **If you want a conflict prompt** (recommended for user-supplied filenames):
664
+
665
+ ```typescript
666
+ try {
667
+ await Koolbase.storage.upload({
668
+ bucket: 'documents',
669
+ path: filename,
670
+ file: { uri, name, type },
671
+ });
672
+ } catch (e) {
673
+ if (e instanceof KoolbaseStorageConflictError) {
674
+ const ok = await confirm(`${e.path} already exists. Overwrite?`);
675
+ if (ok) {
676
+ await Koolbase.storage.upload({
677
+ bucket: 'documents',
678
+ path: filename,
679
+ file: { uri, name, type },
680
+ overwrite: true,
681
+ });
682
+ }
683
+ } else {
684
+ throw e;
685
+ }
686
+ }
687
+ ```
688
+
689
+ **If you used `const { url } = await upload(...)`:**
690
+
691
+ ```typescript
692
+ // Before
693
+ const { url } = await Koolbase.storage.upload({ ... });
694
+
695
+ // After
696
+ const { object, downloadUrl } = await Koolbase.storage.upload({ ... });
697
+ const url = downloadUrl; // if you only want the download URL
698
+ ```
699
+
700
+ **If you catch generic `Error` from storage operations**, consider catching
701
+ `KoolbaseStorageError` (or specific subclasses) for cleaner branching:
702
+
703
+ ```typescript
704
+ try {
705
+ await Koolbase.storage.upload({ ... });
706
+ } catch (e) {
707
+ if (e instanceof KoolbaseStorageConflictError) {
708
+ // Path already exists — prompt user
709
+ } else if (e instanceof KoolbaseStorageNotFoundError) {
710
+ // Bucket missing or deleted
711
+ } else if (e instanceof KoolbaseStoragePermissionError) {
712
+ // Caller not authorized
713
+ } else if (e instanceof KoolbaseStorageError) {
714
+ // Any other storage error
715
+ showError(e.message);
716
+ } else {
717
+ throw e;
718
+ }
719
+ }
720
+ ```
721
+
722
+ ### Server requirements
723
+
724
+ - Requires a Koolbase server build with `PATH_CONFLICT` 409 support and the
725
+ `upload-url` / `confirm` / `download-url` / `object` routes (shipped
726
+ alongside this release).
727
+
728
+ ### Verification recommended
729
+
730
+ Storage in v3.0.0–v4.2.1 was non-functional. v5.0.0 is the first working
731
+ upload path since the security audit. **Test uploads end-to-end on a real
732
+ iOS and Android device after upgrading** — RN's `fetch` Blob-PUT behavior
733
+ can vary subtly by platform.
734
+
735
+ ## 4.2.1
736
+
737
+ ### Fixed
738
+
739
+ - Realtime now delivers `deleted` events. v4.1.0 dropped them because `RealtimeEvent` required a `record`, which deletes don't carry. `record` is now optional and `recordId` is provided on deletes — bringing React Native to parity with Flutter.
740
+
741
+ ## 4.2.0
742
+
743
+ ### Fixed
744
+
745
+ - Realtime now works. The client previously hit the wrong endpoint, authenticated with the public key instead of the user session, never sent a subscribe message, and parsed the wrong event shape — so it delivered nothing. Rewritten to the real protocol: connects with the signed-in user's session, subscribes per collection, streams `created`/`updated` events, and reconnects automatically.
746
+
747
+ ### Changed
748
+
749
+ - `Koolbase.realtime.subscribe(collection, cb)` no longer needs a project — it's derived from the user's session.
750
+
751
+ ## 4.0.0
752
+
753
+ ### Changed
754
+
755
+ - **BREAKING:** `insert` and `update` are now online-first with an offline fallback. When the server is reachable they await the response and throw typed errors on rejection — a unique-constraint conflict throws `KoolbaseConflictError` (with the offending `field`), matching `upsert`. Only a genuine network failure falls back to the optimistic local-cache + sync-queue path. Previously these methods swallowed all server errors and `insert` always returned a local-id optimistic record.
756
+
757
+ ### Migration
758
+
759
+ - Wrap `insert`/`update` in `try/catch` to handle conflicts (`catch (e) { if (e instanceof KoolbaseConflictError) … }`). If you relied on the returned id beginning with `local_`, treat the returned record's `id` as authoritative instead — when online it is now the server id.
760
+
761
+ ## 3.1.0
762
+
763
+ ### Added
764
+
765
+ - `batch()` method on `KoolbaseDatabase` for atomic multi-operation writes — runs `insert` / `update` / `delete` / `upsert` in a single server-side transaction. Closes the parity gap with `koolbase_flutter` 5.0.0, where `batch()` shipped on the Flutter side only. Online-only by design; throws on network failure rather than queuing.
766
+ - `BatchOp` factory and `BatchResult` interface exported from `types`.
767
+
768
+ ## 3.0.0
769
+
770
+ ### BREAKING — security
771
+
772
+ - Data-plane requests (database, storage, functions, offline sync) now
773
+ authenticate with the signed-in user's access token (Authorization: Bearer)
774
+ instead of the x-user-id header. The header is no longer sent or trusted.
775
+ Requires the matching Koolbase server build.
776
+ - End-user identity flows automatically from the active session — nothing to
777
+ pass on db/storage calls. owner/authenticated collections require an active
778
+ session.
779
+
780
+ ### Added
781
+
782
+ - KoolbaseAuth.validAccessToken() — returns a currently-valid token,
783
+ refreshing near expiry; the data-plane clients pull from it per request so
784
+ identity follows the live session.
785
+
786
+ ### Fixed
787
+
788
+ - Offline writes are now replayed with the user's identity (previously synced
789
+ anonymously through the sync engine).
790
+
791
+ ## 2.4.0
792
+
793
+ - **Code Push — mandatory bundles.** The SDK now honors a bundle's `mandatory` flag. When a mandatory bundle is staged:
794
+ - `Koolbase.codePush.hasMandatoryUpdate` returns `true` — read it on app resume to gate your UI.
795
+ - The optional `onMandatoryUpdate` callback on the config passed to `Koolbase.initialize()` fires with `{ version, bundleId }` so you can prompt the user to restart.
796
+ - No breaking changes.
797
+
798
+ ## 2.3.0
799
+
800
+ - Auth errors are now selected from the server's stable error `code` (with
801
+ status/message fallback for older servers), retiring message string-matching.
802
+ - New typed data-layer errors — KoolbaseNotFoundError, KoolbaseValidationError,
803
+ KoolbasePermissionError, KoolbaseRateLimitError — plus a shared
804
+ KoolbaseDataError base. query/get/upsert/deleteWhere now throw these
805
+ (code-first) instead of a generic Error.
806
+ - KoolbaseConflictError now exposes the collided `field` and extends
807
+ KoolbaseDataError.
808
+
809
+ ## 2.2.0
810
+
811
+ - Added `KoolbaseConflictError`, thrown by `upsert` on a unique-constraint violation (HTTP 409). insert/update are optimistic/offline-first and surface conflicts at sync time, not as a thrown error.
812
+
813
+ ## 2.1.0
814
+
815
+ - Added `Koolbase.db.upsert(collection:, match:, data:)` — insert-or-update by a match filter; returns `KoolbaseUpsertResult { record, created }`. Online-only.
816
+ - Added `Koolbase.db.deleteWhere(collection:, filters:)` — bulk delete by filter; returns the number of records deleted. Online-only.
817
+
818
+ ## 2.0.0
819
+
820
+ ### Breaking
821
+
822
+ - **Flat record shape.** Records are no longer wrapped in a `data` envelope on
823
+ the wire — your fields come back top-level, with system metadata in a reserved
824
+ `$`-prefixed namespace (`$id`, `$createdAt`, `$updatedAt`, `$collection`,
825
+ `$createdBy`). The SDK maps this back into `KoolbaseRecord`, so you still read
826
+ fields via `record.data.<field>`.
827
+ - Removed `KoolbaseRecord.projectId` and `KoolbaseRecord.collectionId`.
828
+ - Requires a Koolbase server on the flat record contract (shipped alongside this
829
+ release). Older servers return the legacy envelope and are not compatible.
830
+
831
+ ### Added
832
+
833
+ - `KoolbaseRecord.collection` — the record's collection name.
834
+
835
+ ### Fixed
836
+
837
+ - `KoolbaseRecord.createdAt` / `updatedAt` are now reliably populated. Under the
838
+ previous raw cast they were silently `undefined` (snake_case wire vs camelCase).
839
+
840
+ ### Changed
841
+
842
+ - Realtime events and populated/related records now use the same flat shape.
843
+ - Offline cache is forward-compatible: existing entries stay readable (`id` and
844
+ `data` are shape-stable) and refresh to the new shape on the next online read;
845
+ pending offline writes are preserved.
846
+
847
+ ## 1.11.0
848
+
849
+ ### Added
850
+
851
+ - **Sign in with Google** — production-ready end-user OAuth via
852
+ `Koolbase.auth.signInWithGoogle({idToken, nonce?})`. Routes to the
853
+ server endpoint at `/v1/sdk/auth/oauth/google` with RS256-only JWKS
854
+ verification against Google's certs endpoint, multi-audience support
855
+ (iOS / Android / web client IDs configured per environment),
856
+ 15-minute replay defense, and optional nonce check.
857
+ - `SignInWithGoogleParams` interface in `types.ts`.
858
+ - Three new typed errors in `auth-errors.ts`:
859
+ `GoogleSignInNotConfiguredError`, `InvalidGoogleTokenError`,
860
+ `GoogleEmailRequiredError`. Reuses existing `OAuthEmailConflictError`
861
+ and `UserDisabledError`.
862
+
863
+ #### Example with `@react-native-google-signin/google-signin`
864
+
865
+ ```typescript
866
+ import { GoogleSignin } from '@react-native-google-signin/google-signin';
867
+
868
+ GoogleSignin.configure({
869
+ webClientId: '<your-web-client-id>.apps.googleusercontent.com',
870
+ });
871
+
872
+ const userInfo = await GoogleSignin.signIn();
873
+
874
+ const session = await Koolbase.auth.signInWithGoogle({
875
+ idToken: userInfo.idToken!,
876
+ });
877
+ ```
878
+
879
+ ### Auto-link policy
880
+
881
+ Same as Apple Sign-In (v1.10.0). A new Google identity attaches to an
882
+ existing user only when BOTH the Google email AND the existing user's
883
+ email are verified, AND emails match (case-insensitive). Otherwise
884
+ sign-in either creates a new user (no email collision) or surfaces
885
+ `OAuthEmailConflictError`.
886
+
887
+ ### Configuration required
888
+
889
+ Before users can sign in with Google, configure the provider for your
890
+ environment with the OAuth client IDs from Google Cloud Console (one
891
+ each for iOS, Android, and web). See the README for the SQL setup.
892
+
893
+ ### Coming next
894
+
895
+ - **Dashboard UI** for OAuth config — replaces the SQL workflow
896
+
897
+ ## 1.10.1
898
+
899
+ ### Documentation
900
+
901
+ - README rewritten to accurately reflect the v1.10.0 SDK surface. No SDK
902
+ code changes; this release exists to refresh the README rendered on the
903
+ npmjs.com package page.
904
+ - Removed fictional `Koolbase.auth.signInWithGoogle` reference. Google
905
+ Sign-In is planned for v1.11.0 — noted explicitly in the OAuth section.
906
+ - Replaced the deprecated `KoolbaseAppleAuth.signIn(callback)` example
907
+ with the new `Koolbase.auth.signInWithApple({identityToken, nonce?, fullName?})`
908
+ v1.10.0 API using `@invertase/react-native-apple-authentication`.
909
+ - Added `Koolbase.auth.onAuthStateChange(listener)` example (v1.9.0 feature).
910
+ - Replaced the Firebase/Supabase comparison table with a Koolbase-only
911
+ feature inventory.
912
+ - Bumped install snippet from `^1.8.0` to `^1.10.0`.
913
+
914
+ ## 1.10.0
915
+
916
+ ### Added
917
+
918
+ - **Sign in with Apple** — production-ready end-user OAuth via
919
+ `koolbase.auth.signInWithApple({identityToken, nonce?, fullName?})`.
920
+ Routes to the new server endpoint at `/v1/sdk/auth/oauth/apple` with
921
+ RS256-only JWKS verification, audience bound to your project's iOS
922
+ Bundle ID, 15-minute replay defense, and optional nonce check.
923
+ - `AppleFullName` interface and `SignInWithAppleParams` interface in
924
+ `types.ts`.
925
+ - Four new typed errors in `auth-errors.ts`:
926
+ `AppleSignInNotConfiguredError`, `InvalidAppleTokenError`,
927
+ `AppleEmailRequiredError`, `OAuthEmailConflictError`.
928
+
929
+ #### Example with `@invertase/react-native-apple-authentication`
930
+
931
+ ```typescript
932
+ import appleAuth from '@invertase/react-native-apple-authentication';
933
+
934
+ // Get credential from native Apple Sign-In
935
+ const appleResponse = await appleAuth.performRequest({
936
+ requestedOperation: appleAuth.Operation.LOGIN,
937
+ requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
938
+ });
939
+
940
+ // Pass to Koolbase
941
+ const session = await koolbase.auth.signInWithApple({
942
+ identityToken: appleResponse.identityToken!,
943
+ nonce: appleResponse.nonce,
944
+ fullName: appleResponse.fullName
945
+ ? {
946
+ givenName: appleResponse.fullName.givenName ?? undefined,
947
+ familyName: appleResponse.fullName.familyName ?? undefined,
948
+ }
949
+ : undefined,
950
+ });
951
+ ```
952
+
953
+ ### Auto-link policy
954
+
955
+ A new Apple identity attaches to an existing user only when BOTH the
956
+ provider email AND the existing user's email are verified, AND emails
957
+ match (case-insensitive). Otherwise sign-in either creates a new user
958
+ (no email collision) or surfaces `OAuthEmailConflictError` — user signs
959
+ in with existing method, then links Apple from settings.
960
+
961
+ ### Configuration required
962
+
963
+ Before users can sign in with Apple, configure the provider for your
964
+ environment via direct DB insert (dashboard UI lands in v1.10.x):
965
+
966
+ ```sql
967
+ INSERT INTO project_oauth_configs (environment_id, provider, bundle_id, enabled)
968
+ VALUES ('<your-environment-id>', 'apple', 'com.yourapp.bundle', true);
969
+ ```
970
+
971
+ The Bundle ID is the audience claim in identity tokens from native Apple
972
+ Sign-In and must match exactly.
973
+
974
+ ### Still deprecated — `KoolbaseAppleAuth.signIn` and `oauthLogin`
975
+
976
+ These remain deprecated and throw `KoolbaseAuthError('not_implemented')`.
977
+ The v1.10.0 surface is `koolbase.auth.signInWithApple(...)` on the auth
978
+ instance — same place as all other auth methods.
979
+
980
+ ### Coming next
981
+
982
+ - **Dashboard UI** for OAuth config (v1.10.x) — minimal Bundle-ID input,
983
+ enable/disable toggle.
984
+ - **Google Sign-In** (v1.11.0) — same endpoint pattern at
985
+ `/v1/sdk/auth/oauth/google`.
986
+ - **GitHub OAuth** (v1.12.0) — code-exchange flow.
987
+
988
+ ## 1.9.0
989
+
990
+ ### Fixed (critical)
991
+
992
+ v1.8.0 and earlier shipped with silent breakages on the SDK auth surface.
993
+ Anyone using `KoolbaseAuth` before v1.9.0 should upgrade immediately.
994
+
995
+ - **`x-api-key` header is now sent on every auth request.** Previously the
996
+ SDK sent only `Content-Type`, causing the server's caller middleware to
997
+ resolve every project-scoped call as anonymous — every `/v1/sdk/auth/*`
998
+ endpoint returned 401.
999
+ - **Password reset endpoints were targeting wrong paths.** Both
1000
+ `forgotPassword` (was `/v1/sdk/auth/forgot-password`) and `resetPassword`
1001
+ (was `/v1/sdk/auth/reset-password`) silently 404'd on the server. Now
1002
+ corrected to `/password-reset` and `/password-reset/confirm`.
1003
+ - **Session responses were not being mapped.** The server returns
1004
+ `access_token` / `refresh_token` / `expires_at` (snake_case); the SDK cast
1005
+ directly to camelCase types, so `session.accessToken` was `undefined` and
1006
+ the Authorization header silently sent `Bearer undefined`. Now mapped
1007
+ properly on every session-returning endpoint.
1008
+ - **`register()` was discarding the session** the server returned; only the
1009
+ user object was kept. Now persists the full session.
1010
+
1011
+ ### Added
1012
+
1013
+ - **Persistent sessions.** New `SecureAuthStorage` default backed by
1014
+ `react-native-keychain` (iOS Keychain + Android Keystore-backed
1015
+ encryption). The peer dependency is **optional** — apps without it
1016
+ installed see a clear warning and operate without persistence rather than
1017
+ crashing. Apps with custom requirements (Expo Go, compliance encryption,
1018
+ in-memory test mocks) can implement the `KoolbaseAuthStorage` interface
1019
+ and inject it via `KoolbaseConfig.authStorage`.
1020
+ - **Offline-aware session restoration.** New `restoreSession()` method
1021
+ returning a `RestoreResult` enum:
1022
+ - `NoSession` → no persisted session, show login
1023
+ - `Restored` → ready, show authenticated UI
1024
+ - `Expired` → refresh token rejected, show login
1025
+ - `Offline` → network unreachable, optimistically authenticated
1026
+
1027
+ Optimistic state is populated from disk *before* any network call, so
1028
+ authenticated UI renders immediately at app launch with no round-trip.
1029
+ - **Auth state listener.** New `KoolbaseAuth.onAuthStateChange(listener)`
1030
+ API following the RN ecosystem convention (Firebase/Supabase style):
1031
+ fires immediately on subscribe with current state, then on every state
1032
+ change. Returns an unsubscribe function for cleanup.
1033
+ - **Single-flight token refresh.** Concurrent callers hitting a stale token
1034
+ share one underlying refresh and receive the same result. Prevents the
1035
+ race where parallel refreshes each rotate the refresh token,
1036
+ invalidating peers mid-flight.
1037
+ - **Typed error hierarchy.** 10 new typed errors for granular handling:
1038
+ `InvalidCredentialsError`, `EmailAlreadyInUseError`, `UserDisabledError`,
1039
+ `WeakPasswordError`, `SessionExpiredError`, `TokenRevokedError`,
1040
+ `AccountLockedError` (with forward-compatible `lockedUntil` field),
1041
+ `UnlockTokenInvalidError`, `RateLimitError`, `NetworkError`. All extend
1042
+ `KoolbaseAuthError` for generic catches.
1043
+ - **Account unlock.** New `KoolbaseAuth.unlock(token)` method consumes the
1044
+ one-shot token from a brute-force unlock email and restores login access.
1045
+ - **Device metadata.** Every auth request now carries seven identifying
1046
+ headers including a stable per-install UUID device label, SDK version,
1047
+ platform info, and app version. Helps server-side debugging and
1048
+ version-conditional logic.
1049
+ - **Configurable timeout.** `KoolbaseConfig.authTimeout` (default 10000ms)
1050
+ sets a per-request timeout via `AbortController`.
1051
+ - **Injectable fetch.** `KoolbaseConfig.fetch` accepts an alternate `fetch`
1052
+ implementation. Useful for testing (mock fetch), corporate proxies, or
1053
+ instrumented HTTP.
1054
+ - **`koolbaseSdkVersion` constant** exported for runtime SDK version
1055
+ introspection.
1056
+
1057
+ ### Changed
1058
+
1059
+ - **`logout()` returns `Promise<boolean>`** — `true` if the server-side
1060
+ logout call succeeded, `false` otherwise. Local session is always cleared
1061
+ regardless. Apps that don't care about the server signal can continue to
1062
+ ignore the return value.
1063
+ - **`setSession()` is now async** (returns `Promise<void>`) so storage
1064
+ persistence completes before the call resolves. Source-compatible for
1065
+ callers that ignored the previous void return.
1066
+ - **`register()` validates password length client-side** (must be ≥ 8
1067
+ characters) before hitting the network. Throws `WeakPasswordError`.
1068
+
1069
+ ### Deprecated
1070
+
1071
+ - **`KoolbaseAuth.oauthLogin()` and `KoolbaseAppleAuth.signIn()`** now
1072
+ throw `KoolbaseAuthError('not_implemented')`. The earlier implementations
1073
+ routed through `/v1/auth/oauth` — the dashboard developer OAuth endpoint,
1074
+ which never created project-scoped end-user sessions. Apple Sign-In has
1075
+ therefore never actually worked for SDK consumers since it was first
1076
+ introduced. Proper OAuth (Apple, Google, GitHub) will ship in v1.10.0
1077
+ against new server endpoints at `/v1/sdk/auth/oauth/{provider}`. Use
1078
+ email/password authentication in the meantime.
1079
+
1080
+ ### Peer dependencies
1081
+
1082
+ - `react-native-keychain >= 8.0.0` (**optional** —
1083
+ `peerDependenciesMeta.optional = true`)
1084
+
1085
+ ### Migration
1086
+
1087
+ Most apps work without code changes after upgrading. To opt into
1088
+ persistence, install the peer dependency:
1089
+
1090
+ ```bash
1091
+ npm install react-native-keychain
1092
+ cd ios && pod install
1093
+ ```
1094
+
1095
+ Then call `restoreSession()` at app launch:
1096
+
1097
+ ```typescript
1098
+ useEffect(() => {
1099
+ koolbase.auth.restoreSession().then((result) => {
1100
+ if (result === RestoreResult.Restored) {
1101
+ navigate('Home');
1102
+ } else {
1103
+ navigate('Login');
1104
+ }
1105
+ });
1106
+ }, []);
1107
+ ```
1108
+
1109
+ For apps using Apple Sign-In: temporarily switch to email/password until
1110
+ v1.10.0 ships. The deprecated method now throws explicitly rather than
1111
+ silently failing.
1112
+
1113
+ ## 1.8.0
1114
+
1115
+ ### Added
1116
+
1117
+ - **Functions:** Authenticated invocations now forward the signed-in user's
1118
+ session automatically.
1119
+ - When a user is signed in via `Koolbase.auth`, calls to
1120
+ `Koolbase.functions.invoke()` include their access token in the
1121
+ request.
1122
+ - Functions receive caller identity via `ctx.auth` — an object with
1123
+ `user_id` (string or null) and `is_authenticated` (boolean).
1124
+ - Unauthenticated invokes continue to work; Functions decide whether
1125
+ they require auth and respond with `AUTH_REQUIRED` if needed.
1126
+ - Token refresh is handled transparently — the next invoke after a
1127
+ refresh uses the fresh token without any client-side wiring.
1128
+
1129
+ Backwards compatible: no breaking changes. Existing code paths continue
1130
+ to work.
1131
+
1132
+ ## 1.7.0
1133
+
1134
+ ### Added — Phone + OTP authentication
1135
+
1136
+ Sign users in with their phone number — for emerging markets and apps
1137
+ where email isn't the primary identifier.
1138
+
1139
+ New methods on `Koolbase.auth`:
1140
+
1141
+ - `sendOtp({ phoneNumber })` — sends a 6-digit OTP to an E.164 phone
1142
+ number, returns the expiry timestamp.
1143
+ - `verifyOtp({ phoneNumber, code })` — verifies the code and signs the
1144
+ user in (creates the account if new). Returns `PhoneVerifyResult` with
1145
+ an `isNewUser` flag for routing first-time users to onboarding.
1146
+ - `linkPhone({ phoneNumber, code })` — links a phone number to an
1147
+ already-authenticated user.
1148
+
1149
+ New types: `OtpSendResult`, `PhoneVerifyResult`, `SendOtpParams`,
1150
+ `VerifyOtpParams`, `LinkPhoneParams`.
1151
+
1152
+ `KoolbaseUser` now exposes `phoneNumber` and `phoneVerified` fields.
1153
+
1154
+ New errors (all extend `KoolbaseAuthError`): `InvalidPhoneNumberError`,
1155
+ `OtpExpiredError`, `OtpInvalidError`, `OtpMaxAttemptsError`,
1156
+ `OtpRateLimitError`, `PhoneAlreadyLinkedError`, `SmsConfigMissingError`.
1157
+
1158
+ Phone numbers must be in E.164 format (e.g. `+233244000000`). Configure
1159
+ your SMS provider (Twilio, Africa's Talking, or Hubtel) in the Koolbase
1160
+ dashboard before using.
1161
+
1162
+ ## 1.6.1
1163
+
1164
+ ### Changed
1165
+
1166
+ - README update — Logic Engine v2 operators.
1167
+
1168
+ ## 1.6.0
1169
+
1170
+ ### Added — Logic Engine v2
1171
+
1172
+ Richer conditions with new operators:
1173
+
1174
+ - `gte` — greater than or equals
1175
+ - `lte` — less than or equals
1176
+ - `contains` — string or list contains value
1177
+ - `starts_with` — string starts with
1178
+ - `ends_with` — string ends with
1179
+ - `in_list` — value is in a list
1180
+ - `not_in_list` — value is not in a list
1181
+ - `between` — numeric value in range `[min, max]`
1182
+ - `is_true` — value is boolean true
1183
+ - `is_false` — value is boolean false
1184
+ - `not_exists` — value is null or missing
1185
+
1186
+ All operators work with AND/OR condition groups.
1187
+
1188
+ ## 1.5.0
1189
+
1190
+ ### Added — Sign in with Apple
1191
+
1192
+ > **Note:** This functionality is deprecated as of v1.9.0 — it never
1193
+ > created project-scoped end-user sessions. See the v1.9.0 entry above.
1194
+
1195
+ - Added `KoolbaseAppleAuth.signIn()` — Sign in with Apple for React Native
1196
+ - Added `KoolbaseAuth.oauthLogin()` — unified OAuth login method
1197
+ - Apple identity token verified server-side using Apple's JWKS endpoint
1198
+ - Works with any Apple credential provider (bring your own apple-auth
1199
+ library)
1200
+
1201
+ #### Usage
1202
+
1203
+ ```typescript
1204
+ import { KoolbaseAppleAuth } from 'koolbase-react-native';
1205
+
1206
+ const session = await KoolbaseAppleAuth.signIn(async () => {
1207
+ const credential = await appleAuth.performRequest({
1208
+ requestedOperation: appleAuth.Operation.LOGIN,
1209
+ requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
1210
+ });
1211
+ return credential;
1212
+ });
1213
+ ```
1214
+
1215
+ #### Setup required
1216
+
1217
+ Install `@invertase/react-native-apple-authentication` and configure your
1218
+ App ID in the Apple Developer portal.
1219
+
1220
+ ## 1.4.0
1221
+
1222
+ ### Added — Koolbase Cloud Messaging
1223
+
1224
+ - Added `KoolbaseMessaging` — push notification delivery via FCM.
1225
+ - Added `Koolbase.messaging.registerToken({ token, platform, userId? })`
1226
+ — register FCM device token.
1227
+ - Added `Koolbase.messaging.send({ to, title, body, data? })` — send push
1228
+ notification to a specific device.
1229
+ - `KoolbaseConfig` extended with `messagingEnabled` parameter (default
1230
+ `true`).
1231
+ - Device ID automatically reused from analytics stable device ID
1232
+ (AsyncStorage).
1233
+
1234
+ #### Usage
1235
+
1236
+ ```typescript
1237
+ // After obtaining FCM token from @react-native-firebase/messaging
1238
+ const fcmToken = await messaging().getToken();
1239
+ await Koolbase.messaging.registerToken({
1240
+ token: fcmToken,
1241
+ platform: 'android', // or 'ios'
1242
+ });
1243
+
1244
+ // Send to a specific device
1245
+ await Koolbase.messaging.send({
1246
+ to: deviceToken,
1247
+ title: 'Your order is ready',
1248
+ body: 'Pick up at counter 3',
1249
+ data: { order_id: '123' },
1250
+ });
1251
+ ```
1252
+
1253
+ #### Setup required
1254
+
1255
+ Add your FCM server key as a project secret named `FCM_SERVER_KEY` in the
1256
+ Koolbase dashboard.
1257
+
1258
+ ## 1.3.1
1259
+
1260
+ ### Changed
1261
+
1262
+ - Updated README — added Code Push, Analytics, Logic Engine sections,
1263
+ clearer get started guide.
1264
+
1265
+ ## 1.3.0
1266
+
1267
+ ### Added — Analytics
1268
+
1269
+ - Added `KoolbaseAnalytics` — event tracking with batched flush.
1270
+ - Added `Koolbase.analytics` — top-level accessor.
1271
+ - Added `Koolbase.analytics.track(eventName, properties)` — custom event
1272
+ tracking.
1273
+ - Added `Koolbase.analytics.screenView(screenName, properties)` — screen
1274
+ view tracking.
1275
+ - Added `Koolbase.analytics.identify(userId)` — attach authenticated user.
1276
+ - Added `Koolbase.analytics.setUserProperty(key, value)` — user property.
1277
+ - Added `Koolbase.analytics.setUserProperties(map)` — bulk user
1278
+ properties.
1279
+ - Added `Koolbase.analytics.reset()` — clear identity on logout.
1280
+ - Added `Koolbase.analytics.flush()` — manual flush.
1281
+ - Added `Koolbase.analytics.dispose()` — flush and shut down.
1282
+ - Auto events: `app_open`, `screen_view`, `session_end`.
1283
+ - Batch flush: every 30s, on app background, on close, or at 20 events.
1284
+ - Anonymous by default (stable `device_id` via AsyncStorage), attach
1285
+ `user_id` on `identify()`.
1286
+ - `KoolbaseConfig` extended with `analyticsEnabled` and `appVersion`
1287
+ parameters.
1288
+
1289
+ ### Added — Logic Engine v1
1290
+
1291
+ - Added `Koolbase.executeFlow(flowId, context)` — evaluate named flow
1292
+ from active bundle.
1293
+ - Added `KoolbaseLogicEngine` — safe, deterministic flow evaluator.
1294
+ - Supported node types: `if`, `sequence`, `event` (terminal), `set`.
1295
+ - Supported operators: `eq`, `neq`, `gt`, `lt`, `and`, `or`, `exists`.
1296
+ - Supported data sources: `context` (app-provided), `config` (bundle),
1297
+ `flags` (bundle).
1298
+ - `BundlePayload` extended with `flows` and `screens` fields.
1299
+ - Never throws — returns safe `FlowResult` on any error.
1300
+
1301
+ #### Usage
1302
+
1303
+ ```typescript
1304
+ // Analytics
1305
+ await Koolbase.initialize({
1306
+ publicKey: 'pk_live_xxx',
1307
+ baseUrl: 'https://api.koolbase.com',
1308
+ appVersion: '1.0.0',
1309
+ analyticsEnabled: true,
1310
+ });
1311
+
1312
+ Koolbase.analytics.track('purchase', { value: 1200, currency: 'GHS' });
1313
+ Koolbase.analytics.screenView('checkout');
1314
+ Koolbase.analytics.identify(user.id);
1315
+ Koolbase.analytics.setUserProperty('plan', 'pro');
1316
+
1317
+ // Logic Engine
1318
+ const result = Koolbase.executeFlow('on_checkout_tap', { plan: user.plan });
1319
+ if (result.hasEvent) navigation.navigate(result.eventName!);
1320
+ ```
1321
+
1322
+ ## 1.1.0
1323
+
1324
+ ### Added — Offline-first database
1325
+
1326
+ - Database: offline-first support powered by AsyncStorage.
1327
+ - Cache-first reads — returns local data instantly, refreshes from
1328
+ network in background.
1329
+ - Optimistic writes — inserts saved locally first, synced when online.
1330
+ - Auto-sync on network reconnect via NetInfo.
1331
+ - `Koolbase.db.syncPendingWrites()` — manually trigger sync.
1332
+ - `QueryResult.isFromCache` flag — know whether data came from cache or
1333
+ network.
1334
+ - Write queue with max 3 retries before dropping failed writes.
1335
+ - User-scoped cache — no cross-user data leakage on shared devices.
1336
+ - `PendingWrite` type exported from package.
1337
+
1338
+ ## 1.0.0
1339
+
1340
+ ### Initial release
1341
+
1342
+ - Auth — register, login, logout, current user.
1343
+ - Database — insert, query, get, update, delete, populate.
1344
+ - Storage — upload, download, delete.
1345
+ - Realtime — WebSocket subscriptions.
1346
+ - Functions — invoke deployed functions.
1347
+ - Feature flags and remote config.
1348
+ - Version enforcement.