@sendoracloud/sdk-react-native 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,26 @@
2
2
 
3
3
  Official React Native + Expo SDK for [SendoraCloud](https://sendoracloud.com). Works in Expo Go, managed dev clients, and bare React Native apps.
4
4
 
5
+ > 📖 **First time on React Native?** Read the [5-minute quickstart](https://sendoracloud.com/docs/quickstart-react-native) — it covers the Hermes CSPRNG polyfill, anonymous-first auth, push token receipts, and the demo end-to-end push send.
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
8
10
  npx expo install @sendoracloud/sdk-react-native @react-native-async-storage/async-storage
11
+ npm install react-native-get-random-values
9
12
  ```
10
13
 
11
14
  `@react-native-async-storage/async-storage` is a required peer dependency — used to persist the anonymous device id across app restarts.
12
15
 
16
+ `react-native-get-random-values` is a required runtime polyfill — Hermes (and JavaScriptCore) doesn't expose `crypto.getRandomValues` reliably, and Sendora refuses to mint anonymous IDs from `Math.random`. Add this as the **first import** in your entry file (`index.js` or `index.tsx`):
17
+
18
+ ```ts
19
+ // MUST be the first import in your entry file
20
+ import "react-native-get-random-values";
21
+ ```
22
+
23
+ If you forget, `init()` throws with a paste-ready remediation block — it never fails silently.
24
+
13
25
  ## Quick start
14
26
 
15
27
  ```ts
@@ -36,11 +48,16 @@ SendoraCloud.screen("Pricing");
36
48
  // Register the device push token (get it from expo-notifications)
37
49
  import * as Notifications from "expo-notifications";
38
50
  const { data } = await Notifications.getExpoPushTokenAsync();
39
- await SendoraCloud.registerPushToken({
51
+ const receipt = await SendoraCloud.registerPushToken({
40
52
  token: data,
41
53
  platform: Platform.OS as "ios" | "android",
54
+ // bundleId is OPTIONAL but strongly recommended for multi-env apps:
55
+ // Sendora uses it to route to the right APNs / FCM credentials so a
56
+ // dev token never receives a prod push.
42
57
  bundleId: "com.yourcompany.app",
43
58
  });
59
+ // Returns { tokenId, created } — log it for debugging fan-out issues.
60
+ console.log("registered:", receipt.tokenId, "new?", receipt.created);
44
61
 
45
62
  // On logout
46
63
  SendoraCloud.reset();
@@ -72,6 +89,31 @@ SendoraCloud.reset();
72
89
  }
73
90
  ```
74
91
 
92
+ ## Stability
93
+
94
+ This SDK is on the **0.10.x line**. Patch bumps (0.10.x → 0.10.y) are backwards-compatible; minor bumps may include breaking changes — always read the [CHANGELOG](https://github.com/sendoracloud/sdk-react-native/blob/main/CHANGELOG.md) before upgrading. The next major (1.0) is targeted once the HttpOnly-cookie SSR auth + auto-trait extraction features have soaked in production for two consecutive months without a schema change. Once 1.0 ships, semver is strict.
95
+
96
+ ## JWT verification (custom backend)
97
+
98
+ If your backend verifies Sendora-issued access tokens directly (rather than calling Sendora APIs that re-verify on every hit), use the OIDC-standard JWKS auto-discovery pattern — never hardcode the JWKS URL:
99
+
100
+ ```ts
101
+ import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose";
102
+
103
+ const JWKS_BY_ISS = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
104
+ function getJwks(iss: string) {
105
+ if (!JWKS_BY_ISS.has(iss)) {
106
+ JWKS_BY_ISS.set(iss, createRemoteJWKSet(new URL(`${iss}/.well-known/jwks.json`)));
107
+ }
108
+ return JWKS_BY_ISS.get(iss)!;
109
+ }
110
+
111
+ const claims = decodeJwt(token);
112
+ const { payload } = await jwtVerify(token, getJwks(claims.iss as string));
113
+ ```
114
+
115
+ The `iss` claim is a per-org Sendora URL (e.g. `https://api.sendoracloud.com/api/v1/auth-service/<orgId>`); appending `/.well-known/jwks.json` resolves on the same host. This means a customer can rotate signing keys without your code redeploying. **Don't** copy a hardcoded JWKS URL from a tutorial — it'll break in 12 months when an org rotates.
116
+
75
117
  ## Security
76
118
 
77
119
  - **Secret keys refused on the client.** The SDK only accepts `pk_(live|test)_<alphanumerics>` keys — `sk_*` (any case) or malformed values throw at init. Secret keys must stay on your backend.
package/dist/index.cjs CHANGED
@@ -800,7 +800,7 @@ function decodeJwtPayload(token) {
800
800
  }
801
801
 
802
802
  // src/index.ts
803
- var SDK_VERSION = "0.10.3";
803
+ var SDK_VERSION = "0.10.4";
804
804
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
805
805
  var ANON_KEY = "anon_id";
806
806
  var USER_ID_KEY = "user_id";
@@ -816,15 +816,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
816
816
  var MAX_USER_ID_LEN = 256;
817
817
  var MAX_TRAITS_BYTES = 32 * 1024;
818
818
  var MAX_PROPERTIES_BYTES = 32 * 1024;
819
+ function assertCSPRNG() {
820
+ const g = globalThis;
821
+ if (g.crypto?.getRandomValues) {
822
+ return g.crypto;
823
+ }
824
+ const msg = [
825
+ "[sendoracloud] crypto.getRandomValues is unavailable in this runtime.",
826
+ "",
827
+ "Sendora refuses to mint anonymous IDs from Math.random \u2014 predictable IDs",
828
+ "are a security floor regression, not a tradeoff.",
829
+ "",
830
+ "Fix (React Native + Expo):",
831
+ " 1) npm install react-native-get-random-values",
832
+ " 2) Add this as the FIRST line of your app's entry file (index.js or",
833
+ " index.tsx \u2014 before any other import including Sendora):",
834
+ "",
835
+ " import 'react-native-get-random-values';",
836
+ "",
837
+ " 3) Reload the JS bundle (full restart, not Fast Refresh).",
838
+ "",
839
+ "Docs: https://sendoracloud.com/docs/quickstart-react-native#csprng"
840
+ ].join("\n");
841
+ throw new Error(msg);
842
+ }
819
843
  function uuid() {
820
844
  const bytes = new Uint8Array(16);
821
- const g = globalThis;
822
- if (!g.crypto?.getRandomValues) {
823
- throw new Error(
824
- "[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
825
- );
826
- }
827
- g.crypto.getRandomValues(bytes);
845
+ assertCSPRNG().getRandomValues(bytes);
828
846
  bytes[6] = bytes[6] & 15 | 64;
829
847
  bytes[8] = bytes[8] & 63 | 128;
830
848
  const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
@@ -849,6 +867,7 @@ var SendoraSDK = class {
849
867
  }
850
868
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
851
869
  async init(config) {
870
+ assertCSPRNG();
852
871
  const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
853
872
  if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
854
873
  throw new Error(
@@ -988,7 +1007,9 @@ var SendoraSDK = class {
988
1007
  */
989
1008
  async registerPushToken(reg) {
990
1009
  this.assertReady();
991
- if (!this.config) return;
1010
+ if (!this.config) {
1011
+ throw new Error("[sendoracloud] init() must complete before registerPushToken().");
1012
+ }
992
1013
  if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
993
1014
  throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
994
1015
  }
@@ -998,7 +1019,7 @@ var SendoraSDK = class {
998
1019
  throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
999
1020
  }
1000
1021
  }
1001
- await post(
1022
+ const data = await post(
1002
1023
  { apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
1003
1024
  "/api/v1/push/tokens",
1004
1025
  {
@@ -1011,6 +1032,10 @@ var SendoraSDK = class {
1011
1032
  metadata: reg.metadata ?? {}
1012
1033
  }
1013
1034
  );
1035
+ if (!data?.tokenId) {
1036
+ throw new Error("[sendoracloud] push token registered but server returned no tokenId \u2014 upgrade backend to >= s58.16.");
1037
+ }
1038
+ return { tokenId: data.tokenId, created: data.created === true };
1014
1039
  }
1015
1040
  /**
1016
1041
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
package/dist/index.d.cts CHANGED
@@ -324,11 +324,25 @@ type PushPlatform = "ios" | "android";
324
324
  interface PushTokenRegistration {
325
325
  token: string;
326
326
  platform: PushPlatform;
327
- /** App bundle / package identifier, for fan-out to the right credentials. */
327
+ /**
328
+ * App bundle / package identifier, for fan-out to the right credentials.
329
+ * Optional but strongly recommended for multi-env apps so dev / staging
330
+ * tokens never get a prod APNs / FCM push.
331
+ */
328
332
  bundleId?: string;
329
333
  /** Arbitrary device metadata stored alongside the token. */
330
334
  metadata?: Record<string, string>;
331
335
  }
336
+ /**
337
+ * Returned by `registerPushToken` so the caller can log a receipt or run
338
+ * a follow-up `testPush(userId)` against the freshly stored token. `created`
339
+ * distinguishes a brand-new token row from an idempotent re-registration of
340
+ * a token Sendora already had on file.
341
+ */
342
+ interface PushTokenReceipt {
343
+ tokenId: string;
344
+ created: boolean;
345
+ }
332
346
 
333
347
  /**
334
348
  * @sendoracloud/sdk-react-native — React Native + Expo SDK.
@@ -385,7 +399,7 @@ declare class SendoraSDK {
385
399
  * obtaining the token — typically via `expo-notifications` or
386
400
  * `@react-native-firebase/messaging`.
387
401
  */
388
- registerPushToken(reg: PushTokenRegistration): Promise<void>;
402
+ registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
389
403
  /**
390
404
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
391
405
  * the AsyncStorage writes so a caller who kills the app immediately
@@ -397,4 +411,4 @@ declare class SendoraSDK {
397
411
  }
398
412
  declare const SendoraCloud: SendoraSDK;
399
413
 
400
- export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
414
+ export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
package/dist/index.d.ts CHANGED
@@ -324,11 +324,25 @@ type PushPlatform = "ios" | "android";
324
324
  interface PushTokenRegistration {
325
325
  token: string;
326
326
  platform: PushPlatform;
327
- /** App bundle / package identifier, for fan-out to the right credentials. */
327
+ /**
328
+ * App bundle / package identifier, for fan-out to the right credentials.
329
+ * Optional but strongly recommended for multi-env apps so dev / staging
330
+ * tokens never get a prod APNs / FCM push.
331
+ */
328
332
  bundleId?: string;
329
333
  /** Arbitrary device metadata stored alongside the token. */
330
334
  metadata?: Record<string, string>;
331
335
  }
336
+ /**
337
+ * Returned by `registerPushToken` so the caller can log a receipt or run
338
+ * a follow-up `testPush(userId)` against the freshly stored token. `created`
339
+ * distinguishes a brand-new token row from an idempotent re-registration of
340
+ * a token Sendora already had on file.
341
+ */
342
+ interface PushTokenReceipt {
343
+ tokenId: string;
344
+ created: boolean;
345
+ }
332
346
 
333
347
  /**
334
348
  * @sendoracloud/sdk-react-native — React Native + Expo SDK.
@@ -385,7 +399,7 @@ declare class SendoraSDK {
385
399
  * obtaining the token — typically via `expo-notifications` or
386
400
  * `@react-native-firebase/messaging`.
387
401
  */
388
- registerPushToken(reg: PushTokenRegistration): Promise<void>;
402
+ registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
389
403
  /**
390
404
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
391
405
  * the AsyncStorage writes so a caller who kills the app immediately
@@ -397,4 +411,4 @@ declare class SendoraSDK {
397
411
  }
398
412
  declare const SendoraCloud: SendoraSDK;
399
413
 
400
- export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
414
+ export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
package/dist/index.js CHANGED
@@ -760,7 +760,7 @@ function decodeJwtPayload(token) {
760
760
  }
761
761
 
762
762
  // src/index.ts
763
- var SDK_VERSION = "0.10.3";
763
+ var SDK_VERSION = "0.10.4";
764
764
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
765
765
  var ANON_KEY = "anon_id";
766
766
  var USER_ID_KEY = "user_id";
@@ -776,15 +776,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
776
776
  var MAX_USER_ID_LEN = 256;
777
777
  var MAX_TRAITS_BYTES = 32 * 1024;
778
778
  var MAX_PROPERTIES_BYTES = 32 * 1024;
779
+ function assertCSPRNG() {
780
+ const g = globalThis;
781
+ if (g.crypto?.getRandomValues) {
782
+ return g.crypto;
783
+ }
784
+ const msg = [
785
+ "[sendoracloud] crypto.getRandomValues is unavailable in this runtime.",
786
+ "",
787
+ "Sendora refuses to mint anonymous IDs from Math.random \u2014 predictable IDs",
788
+ "are a security floor regression, not a tradeoff.",
789
+ "",
790
+ "Fix (React Native + Expo):",
791
+ " 1) npm install react-native-get-random-values",
792
+ " 2) Add this as the FIRST line of your app's entry file (index.js or",
793
+ " index.tsx \u2014 before any other import including Sendora):",
794
+ "",
795
+ " import 'react-native-get-random-values';",
796
+ "",
797
+ " 3) Reload the JS bundle (full restart, not Fast Refresh).",
798
+ "",
799
+ "Docs: https://sendoracloud.com/docs/quickstart-react-native#csprng"
800
+ ].join("\n");
801
+ throw new Error(msg);
802
+ }
779
803
  function uuid() {
780
804
  const bytes = new Uint8Array(16);
781
- const g = globalThis;
782
- if (!g.crypto?.getRandomValues) {
783
- throw new Error(
784
- "[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
785
- );
786
- }
787
- g.crypto.getRandomValues(bytes);
805
+ assertCSPRNG().getRandomValues(bytes);
788
806
  bytes[6] = bytes[6] & 15 | 64;
789
807
  bytes[8] = bytes[8] & 63 | 128;
790
808
  const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
@@ -809,6 +827,7 @@ var SendoraSDK = class {
809
827
  }
810
828
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
811
829
  async init(config) {
830
+ assertCSPRNG();
812
831
  const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
813
832
  if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
814
833
  throw new Error(
@@ -948,7 +967,9 @@ var SendoraSDK = class {
948
967
  */
949
968
  async registerPushToken(reg) {
950
969
  this.assertReady();
951
- if (!this.config) return;
970
+ if (!this.config) {
971
+ throw new Error("[sendoracloud] init() must complete before registerPushToken().");
972
+ }
952
973
  if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
953
974
  throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
954
975
  }
@@ -958,7 +979,7 @@ var SendoraSDK = class {
958
979
  throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
959
980
  }
960
981
  }
961
- await post(
982
+ const data = await post(
962
983
  { apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
963
984
  "/api/v1/push/tokens",
964
985
  {
@@ -971,6 +992,10 @@ var SendoraSDK = class {
971
992
  metadata: reg.metadata ?? {}
972
993
  }
973
994
  );
995
+ if (!data?.tokenId) {
996
+ throw new Error("[sendoracloud] push token registered but server returned no tokenId \u2014 upgrade backend to >= s58.16.");
997
+ }
998
+ return { tokenId: data.tokenId, created: data.created === true };
974
999
  }
975
1000
  /**
976
1001
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",