@sendoracloud/sdk-react-native 1.35.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -65,6 +65,12 @@ async function getAsyncStorage() {
65
65
  })();
66
66
  return asyncStoragePromise;
67
67
  }
68
+ var warned = /* @__PURE__ */ new Set();
69
+ function warnOnce(key, message) {
70
+ if (warned.has(key)) return;
71
+ warned.add(key);
72
+ console.warn(message);
73
+ }
68
74
  var Storage = class {
69
75
  constructor(opts = {}) {
70
76
  this.cache = /* @__PURE__ */ new Map();
@@ -73,6 +79,12 @@ var Storage = class {
73
79
  this.secureKeys = new Set(opts.secureKeys ?? []);
74
80
  this.prefix = opts.prefix ?? DEFAULT_PREFIX;
75
81
  this.securePrefix = opts.prefix ?? "";
82
+ if (this.secure === null && this.secureKeys.size > 0) {
83
+ warnOnce(
84
+ "no-secure-adapter",
85
+ "[sendora] No `secureStorage` adapter supplied \u2014 auth tokens (refresh token, access token, cached user) will be stored in PLAIN AsyncStorage, which is included in iOS/Android device backups. Pass `secureStorage` in init() with a Keychain/Keystore-backed adapter (e.g. expo-secure-store). The session still persists either way; this is about where the token rests."
86
+ );
87
+ }
76
88
  }
77
89
  /** The adapter-facing name for a key. Bare by default; see `prefix`. */
78
90
  secureName(key) {
@@ -126,6 +138,10 @@ var Storage = class {
126
138
  }
127
139
  }
128
140
  } catch {
141
+ warnOnce(
142
+ "secure-read-degraded",
143
+ "[sendora] The `secureStorage` adapter failed on read; falling back to plain AsyncStorage for this session. Tokens are preserved but no longer encrypted at rest. Check your adapter."
144
+ );
129
145
  if (store) {
130
146
  try {
131
147
  const legacy = await store.getItem(this.prefix + k);
@@ -142,6 +158,10 @@ var Storage = class {
142
158
  this.cache.set(key, value);
143
159
  if (this.isSecure(key)) {
144
160
  void this.secure.setItem(this.secureName(key), value).catch(() => {
161
+ warnOnce(
162
+ "secure-write-degraded",
163
+ "[sendora] The `secureStorage` adapter failed on write; this value fell back to plain AsyncStorage. The session is preserved but this token is no longer encrypted at rest. Check your adapter."
164
+ );
145
165
  void getAsyncStorage().then((store) => {
146
166
  if (store) store.setItem(this.prefix + key, value).catch(() => {
147
167
  });
@@ -208,7 +228,7 @@ var Storage = class {
208
228
  // package.json
209
229
  var package_default = {
210
230
  name: "@sendoracloud/sdk-react-native",
211
- version: "1.35.0",
231
+ version: "1.36.0",
212
232
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
213
233
  type: "module",
214
234
  main: "./dist/index.cjs",
@@ -496,6 +516,12 @@ function classifyAuthErrorKind(code, status) {
496
516
  return "credential_in_use";
497
517
  case "LAST_CREDENTIAL":
498
518
  return "last_credential";
519
+ // ⚠ MUST be a code case, not a status one. The tail of this function is
520
+ // `if (status >= 400) return "invalid_credential"`, which would otherwise
521
+ // claim the 403 and report a step-up refusal as a bad credential —
522
+ // documented as "needs NEW input", which is the opposite of the truth.
523
+ case "RECENT_AUTH_REQUIRED":
524
+ return "recent_auth_required";
499
525
  case "NOT_ANONYMOUS":
500
526
  case "FORBIDDEN_NON_ANONYMOUS":
501
527
  return "already_identified";
@@ -1582,13 +1608,24 @@ var Auth = class {
1582
1608
  const data = await this.unwrap(res, "MFA_CONFIRM_FAILED");
1583
1609
  return Boolean(data.confirmed);
1584
1610
  }
1611
+ /**
1612
+ * Disable MFA.
1613
+ *
1614
+ * ⚠ STEP-UP gated (s58.319): a stale `auth_time` is refused with 403
1615
+ * `RECENT_AUTH_REQUIRED` / kind `recent_auth_required`.
1616
+ *
1617
+ * ⚠ Discarded its response until s58.335, and `request()` never throws on a
1618
+ * non-2xx (it warns and returns the Response), so the refusal had nowhere to
1619
+ * surface — the app reported MFA as off while TOTP stayed enrolled.
1620
+ */
1585
1621
  async disableMfa() {
1586
- await post(
1622
+ const res = await post(
1587
1623
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1588
1624
  "/api/v1/auth-service/mfa/disable",
1589
1625
  {},
1590
1626
  this.bearerHeaders()
1591
1627
  );
1628
+ await this.unwrap(res, "MFA_DISABLE_FAILED");
1592
1629
  }
1593
1630
  // ============================================================
1594
1631
  // DEVICE SESSIONS (Bearer-authenticated)
@@ -1603,19 +1640,28 @@ var Auth = class {
1603
1640
  const parsed = await res.json();
1604
1641
  return Array.isArray(parsed.data) ? parsed.data : [];
1605
1642
  }
1643
+ /**
1644
+ * Revoke one of the caller's own sessions ("sign out that device").
1645
+ * ⚠ Discarded its response until s58.335 — a "sign out this device" button
1646
+ * that reports success on a failure is the one place a user acts on the claim
1647
+ * and then stops worrying.
1648
+ */
1606
1649
  async revokeSession(sessionId) {
1607
- await del(
1650
+ const res = await del(
1608
1651
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1609
1652
  `/api/v1/auth-service/sessions/me/${encodeURIComponent(sessionId)}`,
1610
1653
  this.bearerHeaders()
1611
1654
  );
1655
+ await this.unwrap(res, "SESSION_REVOKE_FAILED");
1612
1656
  }
1657
+ /** Revoke every session for the caller. Same reporting defect as `revokeSession`. */
1613
1658
  async revokeAllSessions() {
1614
- await del(
1659
+ const res = await del(
1615
1660
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1616
1661
  "/api/v1/auth-service/sessions/me",
1617
1662
  this.bearerHeaders()
1618
1663
  );
1664
+ await this.unwrap(res, "SESSION_REVOKE_ALL_FAILED");
1619
1665
  }
1620
1666
  /**
1621
1667
  * Delete the signed-in user's account (Apple App Store Guideline 5.1.1(v)).
@@ -1781,7 +1827,10 @@ var Auth = class {
1781
1827
  if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
1782
1828
  const parsed = await res.json().catch(() => ({}));
1783
1829
  if (!res.ok || !parsed.success) {
1784
- throw new AuthError(parsed.error?.code ?? errCode, parsed.error?.message ?? `HTTP ${res.status}`);
1830
+ throw new AuthError(parsed.error?.code ?? errCode, parsed.error?.message ?? `HTTP ${res.status}`, {
1831
+ status: res.status,
1832
+ retryAfterSeconds: readRetryAfterSeconds(parsed)
1833
+ });
1785
1834
  }
1786
1835
  return parsed.data;
1787
1836
  }
package/dist/index.d.cts CHANGED
@@ -464,7 +464,7 @@ type UnlinkResult = {
464
464
  * OTP, a dismissed Game Center sheet and a 429 all leave `getUserSync()` and
465
465
  * the stored refresh token exactly as they were.
466
466
  */
467
- type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "already_identified" | "cancelled" | "config" | "unknown";
467
+ type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "recent_auth_required" | "already_identified" | "cancelled" | "config" | "unknown";
468
468
  declare class AuthError extends Error {
469
469
  code: string;
470
470
  /**
@@ -1109,6 +1109,16 @@ declare class Auth {
1109
1109
  recoveryCodes: string[];
1110
1110
  }>;
1111
1111
  confirmMfa(code: string): Promise<boolean>;
1112
+ /**
1113
+ * Disable MFA.
1114
+ *
1115
+ * ⚠ STEP-UP gated (s58.319): a stale `auth_time` is refused with 403
1116
+ * `RECENT_AUTH_REQUIRED` / kind `recent_auth_required`.
1117
+ *
1118
+ * ⚠ Discarded its response until s58.335, and `request()` never throws on a
1119
+ * non-2xx (it warns and returns the Response), so the refusal had nowhere to
1120
+ * surface — the app reported MFA as off while TOTP stayed enrolled.
1121
+ */
1112
1122
  disableMfa(): Promise<void>;
1113
1123
  listMySessions(): Promise<Array<{
1114
1124
  id: string;
@@ -1117,7 +1127,14 @@ declare class Auth {
1117
1127
  lastUsedAt: string | null;
1118
1128
  createdAt: string;
1119
1129
  }>>;
1130
+ /**
1131
+ * Revoke one of the caller's own sessions ("sign out that device").
1132
+ * ⚠ Discarded its response until s58.335 — a "sign out this device" button
1133
+ * that reports success on a failure is the one place a user acts on the claim
1134
+ * and then stops worrying.
1135
+ */
1120
1136
  revokeSession(sessionId: string): Promise<void>;
1137
+ /** Revoke every session for the caller. Same reporting defect as `revokeSession`. */
1121
1138
  revokeAllSessions(): Promise<void>;
1122
1139
  /**
1123
1140
  * Delete the signed-in user's account (Apple App Store Guideline 5.1.1(v)).
package/dist/index.d.ts CHANGED
@@ -464,7 +464,7 @@ type UnlinkResult = {
464
464
  * OTP, a dismissed Game Center sheet and a 429 all leave `getUserSync()` and
465
465
  * the stored refresh token exactly as they were.
466
466
  */
467
- type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "already_identified" | "cancelled" | "config" | "unknown";
467
+ type AuthErrorKind = "network" | "server" | "rate_limited" | "invalid_credential" | "account_locked" | "credential_in_use" | "last_credential" | "recent_auth_required" | "already_identified" | "cancelled" | "config" | "unknown";
468
468
  declare class AuthError extends Error {
469
469
  code: string;
470
470
  /**
@@ -1109,6 +1109,16 @@ declare class Auth {
1109
1109
  recoveryCodes: string[];
1110
1110
  }>;
1111
1111
  confirmMfa(code: string): Promise<boolean>;
1112
+ /**
1113
+ * Disable MFA.
1114
+ *
1115
+ * ⚠ STEP-UP gated (s58.319): a stale `auth_time` is refused with 403
1116
+ * `RECENT_AUTH_REQUIRED` / kind `recent_auth_required`.
1117
+ *
1118
+ * ⚠ Discarded its response until s58.335, and `request()` never throws on a
1119
+ * non-2xx (it warns and returns the Response), so the refusal had nowhere to
1120
+ * surface — the app reported MFA as off while TOTP stayed enrolled.
1121
+ */
1112
1122
  disableMfa(): Promise<void>;
1113
1123
  listMySessions(): Promise<Array<{
1114
1124
  id: string;
@@ -1117,7 +1127,14 @@ declare class Auth {
1117
1127
  lastUsedAt: string | null;
1118
1128
  createdAt: string;
1119
1129
  }>>;
1130
+ /**
1131
+ * Revoke one of the caller's own sessions ("sign out that device").
1132
+ * ⚠ Discarded its response until s58.335 — a "sign out this device" button
1133
+ * that reports success on a failure is the one place a user acts on the claim
1134
+ * and then stops worrying.
1135
+ */
1120
1136
  revokeSession(sessionId: string): Promise<void>;
1137
+ /** Revoke every session for the caller. Same reporting defect as `revokeSession`. */
1121
1138
  revokeAllSessions(): Promise<void>;
1122
1139
  /**
1123
1140
  * Delete the signed-in user's account (Apple App Store Guideline 5.1.1(v)).
package/dist/index.js CHANGED
@@ -20,6 +20,12 @@ async function getAsyncStorage() {
20
20
  })();
21
21
  return asyncStoragePromise;
22
22
  }
23
+ var warned = /* @__PURE__ */ new Set();
24
+ function warnOnce(key, message) {
25
+ if (warned.has(key)) return;
26
+ warned.add(key);
27
+ console.warn(message);
28
+ }
23
29
  var Storage = class {
24
30
  constructor(opts = {}) {
25
31
  this.cache = /* @__PURE__ */ new Map();
@@ -28,6 +34,12 @@ var Storage = class {
28
34
  this.secureKeys = new Set(opts.secureKeys ?? []);
29
35
  this.prefix = opts.prefix ?? DEFAULT_PREFIX;
30
36
  this.securePrefix = opts.prefix ?? "";
37
+ if (this.secure === null && this.secureKeys.size > 0) {
38
+ warnOnce(
39
+ "no-secure-adapter",
40
+ "[sendora] No `secureStorage` adapter supplied \u2014 auth tokens (refresh token, access token, cached user) will be stored in PLAIN AsyncStorage, which is included in iOS/Android device backups. Pass `secureStorage` in init() with a Keychain/Keystore-backed adapter (e.g. expo-secure-store). The session still persists either way; this is about where the token rests."
41
+ );
42
+ }
31
43
  }
32
44
  /** The adapter-facing name for a key. Bare by default; see `prefix`. */
33
45
  secureName(key) {
@@ -81,6 +93,10 @@ var Storage = class {
81
93
  }
82
94
  }
83
95
  } catch {
96
+ warnOnce(
97
+ "secure-read-degraded",
98
+ "[sendora] The `secureStorage` adapter failed on read; falling back to plain AsyncStorage for this session. Tokens are preserved but no longer encrypted at rest. Check your adapter."
99
+ );
84
100
  if (store) {
85
101
  try {
86
102
  const legacy = await store.getItem(this.prefix + k);
@@ -97,6 +113,10 @@ var Storage = class {
97
113
  this.cache.set(key, value);
98
114
  if (this.isSecure(key)) {
99
115
  void this.secure.setItem(this.secureName(key), value).catch(() => {
116
+ warnOnce(
117
+ "secure-write-degraded",
118
+ "[sendora] The `secureStorage` adapter failed on write; this value fell back to plain AsyncStorage. The session is preserved but this token is no longer encrypted at rest. Check your adapter."
119
+ );
100
120
  void getAsyncStorage().then((store) => {
101
121
  if (store) store.setItem(this.prefix + key, value).catch(() => {
102
122
  });
@@ -163,7 +183,7 @@ var Storage = class {
163
183
  // package.json
164
184
  var package_default = {
165
185
  name: "@sendoracloud/sdk-react-native",
166
- version: "1.35.0",
186
+ version: "1.36.0",
167
187
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
168
188
  type: "module",
169
189
  main: "./dist/index.cjs",
@@ -451,6 +471,12 @@ function classifyAuthErrorKind(code, status) {
451
471
  return "credential_in_use";
452
472
  case "LAST_CREDENTIAL":
453
473
  return "last_credential";
474
+ // ⚠ MUST be a code case, not a status one. The tail of this function is
475
+ // `if (status >= 400) return "invalid_credential"`, which would otherwise
476
+ // claim the 403 and report a step-up refusal as a bad credential —
477
+ // documented as "needs NEW input", which is the opposite of the truth.
478
+ case "RECENT_AUTH_REQUIRED":
479
+ return "recent_auth_required";
454
480
  case "NOT_ANONYMOUS":
455
481
  case "FORBIDDEN_NON_ANONYMOUS":
456
482
  return "already_identified";
@@ -1537,13 +1563,24 @@ var Auth = class {
1537
1563
  const data = await this.unwrap(res, "MFA_CONFIRM_FAILED");
1538
1564
  return Boolean(data.confirmed);
1539
1565
  }
1566
+ /**
1567
+ * Disable MFA.
1568
+ *
1569
+ * ⚠ STEP-UP gated (s58.319): a stale `auth_time` is refused with 403
1570
+ * `RECENT_AUTH_REQUIRED` / kind `recent_auth_required`.
1571
+ *
1572
+ * ⚠ Discarded its response until s58.335, and `request()` never throws on a
1573
+ * non-2xx (it warns and returns the Response), so the refusal had nowhere to
1574
+ * surface — the app reported MFA as off while TOTP stayed enrolled.
1575
+ */
1540
1576
  async disableMfa() {
1541
- await post(
1577
+ const res = await post(
1542
1578
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1543
1579
  "/api/v1/auth-service/mfa/disable",
1544
1580
  {},
1545
1581
  this.bearerHeaders()
1546
1582
  );
1583
+ await this.unwrap(res, "MFA_DISABLE_FAILED");
1547
1584
  }
1548
1585
  // ============================================================
1549
1586
  // DEVICE SESSIONS (Bearer-authenticated)
@@ -1558,19 +1595,28 @@ var Auth = class {
1558
1595
  const parsed = await res.json();
1559
1596
  return Array.isArray(parsed.data) ? parsed.data : [];
1560
1597
  }
1598
+ /**
1599
+ * Revoke one of the caller's own sessions ("sign out that device").
1600
+ * ⚠ Discarded its response until s58.335 — a "sign out this device" button
1601
+ * that reports success on a failure is the one place a user acts on the claim
1602
+ * and then stops worrying.
1603
+ */
1561
1604
  async revokeSession(sessionId) {
1562
- await del(
1605
+ const res = await del(
1563
1606
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1564
1607
  `/api/v1/auth-service/sessions/me/${encodeURIComponent(sessionId)}`,
1565
1608
  this.bearerHeaders()
1566
1609
  );
1610
+ await this.unwrap(res, "SESSION_REVOKE_FAILED");
1567
1611
  }
1612
+ /** Revoke every session for the caller. Same reporting defect as `revokeSession`. */
1568
1613
  async revokeAllSessions() {
1569
- await del(
1614
+ const res = await del(
1570
1615
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
1571
1616
  "/api/v1/auth-service/sessions/me",
1572
1617
  this.bearerHeaders()
1573
1618
  );
1619
+ await this.unwrap(res, "SESSION_REVOKE_ALL_FAILED");
1574
1620
  }
1575
1621
  /**
1576
1622
  * Delete the signed-in user's account (Apple App Store Guideline 5.1.1(v)).
@@ -1736,7 +1782,10 @@ var Auth = class {
1736
1782
  if (!res) throw new AuthError("NETWORK_ERROR", "Network request failed");
1737
1783
  const parsed = await res.json().catch(() => ({}));
1738
1784
  if (!res.ok || !parsed.success) {
1739
- throw new AuthError(parsed.error?.code ?? errCode, parsed.error?.message ?? `HTTP ${res.status}`);
1785
+ throw new AuthError(parsed.error?.code ?? errCode, parsed.error?.message ?? `HTTP ${res.status}`, {
1786
+ status: res.status,
1787
+ retryAfterSeconds: readRetryAfterSeconds(parsed)
1788
+ });
1740
1789
  }
1741
1790
  return parsed.data;
1742
1791
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",