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