@sendoracloud/sdk-react-native 0.10.2 → 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 +43 -1
- package/dist/index.cjs +47 -12
- package/dist/index.d.cts +35 -4
- package/dist/index.d.ts +35 -4
- package/dist/index.js +47 -12
- package/package.json +1 -1
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
|
@@ -231,9 +231,14 @@ var Auth = class {
|
|
|
231
231
|
this.inflight = Promise.resolve();
|
|
232
232
|
this.refreshInflight = null;
|
|
233
233
|
this.hooks = hooks;
|
|
234
|
+
this.readyPromise = new Promise((res) => {
|
|
235
|
+
this.resolveReady = res;
|
|
236
|
+
});
|
|
234
237
|
}
|
|
235
238
|
/** Re-hydrate session from storage. Called by the parent SDK
|
|
236
|
-
* during init() after Storage.hydrate() has run.
|
|
239
|
+
* during init() after Storage.hydrate() has run. Marks the
|
|
240
|
+
* internal ready promise as resolved so queued auth calls
|
|
241
|
+
* unblock. */
|
|
237
242
|
hydrate() {
|
|
238
243
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
239
244
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
@@ -253,6 +258,7 @@ var Auth = class {
|
|
|
253
258
|
this.hooks.storage.remove(REFRESH_KEY);
|
|
254
259
|
}
|
|
255
260
|
}
|
|
261
|
+
this.resolveReady();
|
|
256
262
|
}
|
|
257
263
|
getCurrentUser() {
|
|
258
264
|
return this.user;
|
|
@@ -705,7 +711,11 @@ var Auth = class {
|
|
|
705
711
|
}
|
|
706
712
|
// --- internals ---
|
|
707
713
|
serialize(op) {
|
|
708
|
-
const
|
|
714
|
+
const guarded = async () => {
|
|
715
|
+
await this.readyPromise;
|
|
716
|
+
return op();
|
|
717
|
+
};
|
|
718
|
+
const next = this.inflight.then(guarded, guarded);
|
|
709
719
|
this.inflight = next.catch(() => void 0);
|
|
710
720
|
return next;
|
|
711
721
|
}
|
|
@@ -790,7 +800,7 @@ function decodeJwtPayload(token) {
|
|
|
790
800
|
}
|
|
791
801
|
|
|
792
802
|
// src/index.ts
|
|
793
|
-
var SDK_VERSION = "0.10.
|
|
803
|
+
var SDK_VERSION = "0.10.4";
|
|
794
804
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
795
805
|
var ANON_KEY = "anon_id";
|
|
796
806
|
var USER_ID_KEY = "user_id";
|
|
@@ -806,15 +816,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
806
816
|
var MAX_USER_ID_LEN = 256;
|
|
807
817
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
808
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
|
+
}
|
|
809
843
|
function uuid() {
|
|
810
844
|
const bytes = new Uint8Array(16);
|
|
811
|
-
|
|
812
|
-
if (!g.crypto?.getRandomValues) {
|
|
813
|
-
throw new Error(
|
|
814
|
-
"[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
g.crypto.getRandomValues(bytes);
|
|
845
|
+
assertCSPRNG().getRandomValues(bytes);
|
|
818
846
|
bytes[6] = bytes[6] & 15 | 64;
|
|
819
847
|
bytes[8] = bytes[8] & 63 | 128;
|
|
820
848
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -839,6 +867,7 @@ var SendoraSDK = class {
|
|
|
839
867
|
}
|
|
840
868
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
841
869
|
async init(config) {
|
|
870
|
+
assertCSPRNG();
|
|
842
871
|
const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
|
|
843
872
|
if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
|
|
844
873
|
throw new Error(
|
|
@@ -978,7 +1007,9 @@ var SendoraSDK = class {
|
|
|
978
1007
|
*/
|
|
979
1008
|
async registerPushToken(reg) {
|
|
980
1009
|
this.assertReady();
|
|
981
|
-
if (!this.config)
|
|
1010
|
+
if (!this.config) {
|
|
1011
|
+
throw new Error("[sendoracloud] init() must complete before registerPushToken().");
|
|
1012
|
+
}
|
|
982
1013
|
if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
|
|
983
1014
|
throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
|
|
984
1015
|
}
|
|
@@ -988,7 +1019,7 @@ var SendoraSDK = class {
|
|
|
988
1019
|
throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
|
|
989
1020
|
}
|
|
990
1021
|
}
|
|
991
|
-
await post(
|
|
1022
|
+
const data = await post(
|
|
992
1023
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
993
1024
|
"/api/v1/push/tokens",
|
|
994
1025
|
{
|
|
@@ -1001,6 +1032,10 @@ var SendoraSDK = class {
|
|
|
1001
1032
|
metadata: reg.metadata ?? {}
|
|
1002
1033
|
}
|
|
1003
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 };
|
|
1004
1039
|
}
|
|
1005
1040
|
/**
|
|
1006
1041
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
package/dist/index.d.cts
CHANGED
|
@@ -106,9 +106,26 @@ declare class Auth {
|
|
|
106
106
|
private accessExpiresAt;
|
|
107
107
|
private inflight;
|
|
108
108
|
private refreshInflight;
|
|
109
|
+
/**
|
|
110
|
+
* Resolves when the parent SDK has finished its async init() —
|
|
111
|
+
* AsyncStorage hydrated + auth.hydrate() restored the cached
|
|
112
|
+
* session. All public auth methods await this internally so a
|
|
113
|
+
* customer who forgets `await sendora.init()` before showing the
|
|
114
|
+
* signup UI doesn't hit the cold-start race where the upgrade
|
|
115
|
+
* path's `storage.get(REFRESH_KEY)` returns null even though a
|
|
116
|
+
* valid anonymous session is sitting on disk.
|
|
117
|
+
*
|
|
118
|
+
* Industry-standard "SDK gates on internal readiness" pattern
|
|
119
|
+
* (Firebase Auth, Auth0, Clerk all do equivalent internal
|
|
120
|
+
* gating).
|
|
121
|
+
*/
|
|
122
|
+
private readyPromise;
|
|
123
|
+
private resolveReady;
|
|
109
124
|
constructor(hooks: AuthHooks);
|
|
110
125
|
/** Re-hydrate session from storage. Called by the parent SDK
|
|
111
|
-
* during init() after Storage.hydrate() has run.
|
|
126
|
+
* during init() after Storage.hydrate() has run. Marks the
|
|
127
|
+
* internal ready promise as resolved so queued auth calls
|
|
128
|
+
* unblock. */
|
|
112
129
|
hydrate(): void;
|
|
113
130
|
getCurrentUser(): AuthUser | null;
|
|
114
131
|
/**
|
|
@@ -307,11 +324,25 @@ type PushPlatform = "ios" | "android";
|
|
|
307
324
|
interface PushTokenRegistration {
|
|
308
325
|
token: string;
|
|
309
326
|
platform: PushPlatform;
|
|
310
|
-
/**
|
|
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
|
+
*/
|
|
311
332
|
bundleId?: string;
|
|
312
333
|
/** Arbitrary device metadata stored alongside the token. */
|
|
313
334
|
metadata?: Record<string, string>;
|
|
314
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
|
+
}
|
|
315
346
|
|
|
316
347
|
/**
|
|
317
348
|
* @sendoracloud/sdk-react-native — React Native + Expo SDK.
|
|
@@ -368,7 +399,7 @@ declare class SendoraSDK {
|
|
|
368
399
|
* obtaining the token — typically via `expo-notifications` or
|
|
369
400
|
* `@react-native-firebase/messaging`.
|
|
370
401
|
*/
|
|
371
|
-
registerPushToken(reg: PushTokenRegistration): Promise<
|
|
402
|
+
registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
|
|
372
403
|
/**
|
|
373
404
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
|
374
405
|
* the AsyncStorage writes so a caller who kills the app immediately
|
|
@@ -380,4 +411,4 @@ declare class SendoraSDK {
|
|
|
380
411
|
}
|
|
381
412
|
declare const SendoraCloud: SendoraSDK;
|
|
382
413
|
|
|
383
|
-
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
|
@@ -106,9 +106,26 @@ declare class Auth {
|
|
|
106
106
|
private accessExpiresAt;
|
|
107
107
|
private inflight;
|
|
108
108
|
private refreshInflight;
|
|
109
|
+
/**
|
|
110
|
+
* Resolves when the parent SDK has finished its async init() —
|
|
111
|
+
* AsyncStorage hydrated + auth.hydrate() restored the cached
|
|
112
|
+
* session. All public auth methods await this internally so a
|
|
113
|
+
* customer who forgets `await sendora.init()` before showing the
|
|
114
|
+
* signup UI doesn't hit the cold-start race where the upgrade
|
|
115
|
+
* path's `storage.get(REFRESH_KEY)` returns null even though a
|
|
116
|
+
* valid anonymous session is sitting on disk.
|
|
117
|
+
*
|
|
118
|
+
* Industry-standard "SDK gates on internal readiness" pattern
|
|
119
|
+
* (Firebase Auth, Auth0, Clerk all do equivalent internal
|
|
120
|
+
* gating).
|
|
121
|
+
*/
|
|
122
|
+
private readyPromise;
|
|
123
|
+
private resolveReady;
|
|
109
124
|
constructor(hooks: AuthHooks);
|
|
110
125
|
/** Re-hydrate session from storage. Called by the parent SDK
|
|
111
|
-
* during init() after Storage.hydrate() has run.
|
|
126
|
+
* during init() after Storage.hydrate() has run. Marks the
|
|
127
|
+
* internal ready promise as resolved so queued auth calls
|
|
128
|
+
* unblock. */
|
|
112
129
|
hydrate(): void;
|
|
113
130
|
getCurrentUser(): AuthUser | null;
|
|
114
131
|
/**
|
|
@@ -307,11 +324,25 @@ type PushPlatform = "ios" | "android";
|
|
|
307
324
|
interface PushTokenRegistration {
|
|
308
325
|
token: string;
|
|
309
326
|
platform: PushPlatform;
|
|
310
|
-
/**
|
|
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
|
+
*/
|
|
311
332
|
bundleId?: string;
|
|
312
333
|
/** Arbitrary device metadata stored alongside the token. */
|
|
313
334
|
metadata?: Record<string, string>;
|
|
314
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
|
+
}
|
|
315
346
|
|
|
316
347
|
/**
|
|
317
348
|
* @sendoracloud/sdk-react-native — React Native + Expo SDK.
|
|
@@ -368,7 +399,7 @@ declare class SendoraSDK {
|
|
|
368
399
|
* obtaining the token — typically via `expo-notifications` or
|
|
369
400
|
* `@react-native-firebase/messaging`.
|
|
370
401
|
*/
|
|
371
|
-
registerPushToken(reg: PushTokenRegistration): Promise<
|
|
402
|
+
registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
|
|
372
403
|
/**
|
|
373
404
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
|
374
405
|
* the AsyncStorage writes so a caller who kills the app immediately
|
|
@@ -380,4 +411,4 @@ declare class SendoraSDK {
|
|
|
380
411
|
}
|
|
381
412
|
declare const SendoraCloud: SendoraSDK;
|
|
382
413
|
|
|
383
|
-
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
|
@@ -191,9 +191,14 @@ var Auth = class {
|
|
|
191
191
|
this.inflight = Promise.resolve();
|
|
192
192
|
this.refreshInflight = null;
|
|
193
193
|
this.hooks = hooks;
|
|
194
|
+
this.readyPromise = new Promise((res) => {
|
|
195
|
+
this.resolveReady = res;
|
|
196
|
+
});
|
|
194
197
|
}
|
|
195
198
|
/** Re-hydrate session from storage. Called by the parent SDK
|
|
196
|
-
* during init() after Storage.hydrate() has run.
|
|
199
|
+
* during init() after Storage.hydrate() has run. Marks the
|
|
200
|
+
* internal ready promise as resolved so queued auth calls
|
|
201
|
+
* unblock. */
|
|
197
202
|
hydrate() {
|
|
198
203
|
const cachedUser = this.hooks.storage.get(USER_KEY);
|
|
199
204
|
const cachedToken = this.hooks.storage.get(TOKEN_KEY);
|
|
@@ -213,6 +218,7 @@ var Auth = class {
|
|
|
213
218
|
this.hooks.storage.remove(REFRESH_KEY);
|
|
214
219
|
}
|
|
215
220
|
}
|
|
221
|
+
this.resolveReady();
|
|
216
222
|
}
|
|
217
223
|
getCurrentUser() {
|
|
218
224
|
return this.user;
|
|
@@ -665,7 +671,11 @@ var Auth = class {
|
|
|
665
671
|
}
|
|
666
672
|
// --- internals ---
|
|
667
673
|
serialize(op) {
|
|
668
|
-
const
|
|
674
|
+
const guarded = async () => {
|
|
675
|
+
await this.readyPromise;
|
|
676
|
+
return op();
|
|
677
|
+
};
|
|
678
|
+
const next = this.inflight.then(guarded, guarded);
|
|
669
679
|
this.inflight = next.catch(() => void 0);
|
|
670
680
|
return next;
|
|
671
681
|
}
|
|
@@ -750,7 +760,7 @@ function decodeJwtPayload(token) {
|
|
|
750
760
|
}
|
|
751
761
|
|
|
752
762
|
// src/index.ts
|
|
753
|
-
var SDK_VERSION = "0.10.
|
|
763
|
+
var SDK_VERSION = "0.10.4";
|
|
754
764
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
755
765
|
var ANON_KEY = "anon_id";
|
|
756
766
|
var USER_ID_KEY = "user_id";
|
|
@@ -766,15 +776,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
766
776
|
var MAX_USER_ID_LEN = 256;
|
|
767
777
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
768
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
|
+
}
|
|
769
803
|
function uuid() {
|
|
770
804
|
const bytes = new Uint8Array(16);
|
|
771
|
-
|
|
772
|
-
if (!g.crypto?.getRandomValues) {
|
|
773
|
-
throw new Error(
|
|
774
|
-
"[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
g.crypto.getRandomValues(bytes);
|
|
805
|
+
assertCSPRNG().getRandomValues(bytes);
|
|
778
806
|
bytes[6] = bytes[6] & 15 | 64;
|
|
779
807
|
bytes[8] = bytes[8] & 63 | 128;
|
|
780
808
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -799,6 +827,7 @@ var SendoraSDK = class {
|
|
|
799
827
|
}
|
|
800
828
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
801
829
|
async init(config) {
|
|
830
|
+
assertCSPRNG();
|
|
802
831
|
const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
|
|
803
832
|
if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
|
|
804
833
|
throw new Error(
|
|
@@ -938,7 +967,9 @@ var SendoraSDK = class {
|
|
|
938
967
|
*/
|
|
939
968
|
async registerPushToken(reg) {
|
|
940
969
|
this.assertReady();
|
|
941
|
-
if (!this.config)
|
|
970
|
+
if (!this.config) {
|
|
971
|
+
throw new Error("[sendoracloud] init() must complete before registerPushToken().");
|
|
972
|
+
}
|
|
942
973
|
if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
|
|
943
974
|
throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
|
|
944
975
|
}
|
|
@@ -948,7 +979,7 @@ var SendoraSDK = class {
|
|
|
948
979
|
throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
|
|
949
980
|
}
|
|
950
981
|
}
|
|
951
|
-
await post(
|
|
982
|
+
const data = await post(
|
|
952
983
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
953
984
|
"/api/v1/push/tokens",
|
|
954
985
|
{
|
|
@@ -961,6 +992,10 @@ var SendoraSDK = class {
|
|
|
961
992
|
metadata: reg.metadata ?? {}
|
|
962
993
|
}
|
|
963
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 };
|
|
964
999
|
}
|
|
965
1000
|
/**
|
|
966
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
|
+
"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",
|