@layers/amba-react-native 4.2.1 → 4.2.2

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
@@ -249,8 +249,35 @@ function withSignal(init, signal) {
249
249
  //#region src/http-client.ts
250
250
  /** Default amba API endpoint. */
251
251
  const DEFAULT_BASE_URL = "https://api.amba.dev";
252
- /** Server codes for a session token the server no longer accepts. */
253
- const SESSION_REFUSED_CODES = new Set(["SESSION_REVOKED", "INVALID_SESSION_TOKEN"]);
252
+ /**
253
+ * Server codes for a session token the server no longer accepts.
254
+ * `INVALID_SESSION` is the code the auth routes that verify the session
255
+ * themselves (`/identifiers`, `/link`, `/unlink`) answer with.
256
+ */
257
+ const SESSION_REFUSED_CODES = new Set([
258
+ "SESSION_REVOKED",
259
+ "INVALID_SESSION_TOKEN",
260
+ "INVALID_SESSION"
261
+ ]);
262
+ /** True for a server error that refused the session token a call sent. */
263
+ function isSessionRefusedError(err) {
264
+ return err instanceof AmbaApiError && SESSION_REFUSED_CODES.has(err.code);
265
+ }
266
+ const AUTH_PATH_PREFIX = "/v1/client/auth/";
267
+ /**
268
+ * The auth routes that read the session from the `Authorization` header and
269
+ * mint nothing: a refused session on them recovers like it does on any
270
+ * other authenticated call. The other auth routes stay out of the
271
+ * recovery: `/refresh` is the recovery itself, `/logout` ends the session,
272
+ * the sign-in, sign-up, OTP and magic-link routes take credentials (a 401
273
+ * there is a credential error), and `/link` and `/unlink` carry the session
274
+ * in their body, so their callers recover and rebuild the body themselves.
275
+ */
276
+ const SESSION_HEADER_AUTH_PATHS = new Set([`${AUTH_PATH_PREFIX}me`, `${AUTH_PATH_PREFIX}identifiers`]);
277
+ /** Whether a refused session on `path` goes through `onSessionRefused`. */
278
+ function recoversRefusedSession(path) {
279
+ return !path.startsWith(AUTH_PATH_PREFIX) || SESSION_HEADER_AUTH_PATHS.has(path);
280
+ }
254
281
  var AmbaHttpClient = class {
255
282
  apiKey;
256
283
  baseUrl;
@@ -386,7 +413,7 @@ var AmbaHttpClient = class {
386
413
  try {
387
414
  return parseResponse(response, text);
388
415
  } catch (err) {
389
- if (!resent && storedToken && this.onSessionRefused && !path.startsWith("/v1/client/auth/") && err instanceof AmbaApiError && SESSION_REFUSED_CODES.has(err.code) && await this.recoverSession(storedToken, storedUserId)) return this.request(method, path, body, query, void 0, true);
416
+ if (!resent && storedToken && this.onSessionRefused && recoversRefusedSession(path) && isSessionRefusedError(err) && await this.recoverSession(storedToken, storedUserId)) return this.request(method, path, body, query, void 0, true);
390
417
  throw err;
391
418
  }
392
419
  }
@@ -2747,7 +2774,7 @@ function delay(ms) {
2747
2774
  //#endregion
2748
2775
  //#region src/index.ts
2749
2776
  /** SDK semver — burned in at build time. Bump alongside package.json. */
2750
- const SDK_VERSION = "4.2.1";
2777
+ const SDK_VERSION = "4.2.2";
2751
2778
  /**
2752
2779
  * Default host that serves deployed functions. `Amba.functions.invoke(name)`
2753
2780
  * targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
@@ -3033,7 +3060,11 @@ function trackLink(state_, link) {
3033
3060
  }
3034
3061
  /**
3035
3062
  * After the server refused the stored session token on an authenticated
3036
- * call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`). True when a usable
3063
+ * call (`SESSION_REVOKED`, `INVALID_SESSION_TOKEN`, `INVALID_SESSION`). The
3064
+ * HTTP client runs it for calls that send the session in the
3065
+ * `Authorization` header, `/v1/client/auth/me` and `/identifiers`
3066
+ * included; `withBodySession` runs it for the link and unlink calls, which
3067
+ * send the session in their body. True when a usable
3037
3068
  * session for the user the call was sent for is stored afterwards, so the
3038
3069
  * call is sent once more as that user:
3039
3070
  * - a link in flight on this device stores the pair it returns, which is
@@ -3067,6 +3098,40 @@ async function recoverRefusedSession(state_, usedToken, usedUserId) {
3067
3098
  return usableForCaller(state_.identity.getSessionToken());
3068
3099
  }
3069
3100
  /**
3101
+ * Send an auth call that carries the stored session token in its body
3102
+ * (`/link`, `/unlink`). `send` builds the request from the token it is
3103
+ * given. When the server refuses that token, the session recovers through
3104
+ * `recoverRefusedSession` (one refresh shared with every other refused
3105
+ * call) and the call is sent once more with the recovered token; a second
3106
+ * refusal reaches the app. A link waits here outside `trackLink`: its own
3107
+ * failed attempt has already left `linksInflight` (the removal is the first
3108
+ * reaction on that promise), so recovery never waits on itself.
3109
+ */
3110
+ async function withBodySession(state_, send) {
3111
+ const usedToken = state_.identity.getSessionToken();
3112
+ if (!usedToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3113
+ const usedUserId = state_.identity.getAppUserId();
3114
+ try {
3115
+ return await send(usedToken);
3116
+ } catch (err) {
3117
+ if (!isSessionRefusedError(err)) throw err;
3118
+ if (!await recoverRefusedSession(state_, usedToken, usedUserId)) throw err;
3119
+ const recovered = state_.identity.getSessionToken();
3120
+ if (!recovered) throw err;
3121
+ return send(recovered);
3122
+ }
3123
+ }
3124
+ /**
3125
+ * Before a read of the current session (`me`, `getSession`,
3126
+ * `getCurrentIdentifiers`) while no session token is stored: wait for the
3127
+ * launch restore or sign-in in flight, so a check made at launch sees the
3128
+ * session it restores.
3129
+ */
3130
+ async function settleSessionInFlight(state_) {
3131
+ if (state_.identity.getSessionToken() || !state_.anonSignInInflight) return;
3132
+ await state_.anonSignInInflight.catch(() => {});
3133
+ }
3134
+ /**
3070
3135
  * Continue the stored session with its refresh token. True when it did.
3071
3136
  * False when there is no refresh token, or the server rejected it: the
3072
3137
  * session is then cleared (the anonymous id rotates unless the session was
@@ -3177,7 +3242,10 @@ async function refreshSessionOnce(state_) {
3177
3242
  * account's rotated anonymous id is adopted.
3178
3243
  */
3179
3244
  function postLink(state_, body) {
3180
- return trackLink(state_, postLinkOnce(state_, body));
3245
+ return withBodySession(state_, (sessionToken) => trackLink(state_, postLinkOnce(state_, {
3246
+ ...body,
3247
+ session_token: sessionToken
3248
+ })));
3181
3249
  }
3182
3250
  async function postLinkOnce(state_, body) {
3183
3251
  const env = await state_.http.post(P.authLink, {
@@ -3213,12 +3281,10 @@ async function postUserNoRotate(state_, path, body) {
3213
3281
  }
3214
3282
  async function unlinkProvider(provider) {
3215
3283
  const s = getState();
3216
- const sessionToken = s.identity.getSessionToken();
3217
- if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3218
- return postUserNoRotate(s, P.authUnlink, {
3284
+ return withBodySession(s, (sessionToken) => postUserNoRotate(s, P.authUnlink, {
3219
3285
  provider,
3220
3286
  session_token: sessionToken
3221
- });
3287
+ }));
3222
3288
  }
3223
3289
  function requireEmail(email) {
3224
3290
  const trimmed = email.trim();
@@ -3732,52 +3798,38 @@ const Amba = {
3732
3798
  if (providerTrimmed === "") throw new AmbaApiError("VALIDATION_ERROR", "provider must not be empty");
3733
3799
  if (typeof credential !== "string" || credential.trim() === "") throw new AmbaApiError("VALIDATION_ERROR", "credential must not be empty");
3734
3800
  const s = getState();
3735
- const sessionToken = s.identity.getSessionToken();
3736
- if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3737
- return trackLink(s, postAuth(s, P.authLink, {
3801
+ return withBodySession(s, (sessionToken) => trackLink(s, postAuth(s, P.authLink, {
3738
3802
  provider: providerTrimmed,
3739
3803
  token: credential,
3740
3804
  session_token: sessionToken,
3741
3805
  ...nonceField(options)
3742
- }));
3806
+ })));
3743
3807
  },
3744
3808
  linkSmsOtp: async (phone, code) => {
3745
3809
  requireE164Phone(phone);
3746
3810
  requireOtpCode(code);
3747
- const s = getState();
3748
- const sessionToken = s.identity.getSessionToken();
3749
- if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3750
- return postLink(s, {
3811
+ return postLink(getState(), {
3751
3812
  provider: "sms_otp",
3752
3813
  phone,
3753
- code,
3754
- session_token: sessionToken
3814
+ code
3755
3815
  });
3756
3816
  },
3757
3817
  linkEmailOtp: async (email, code) => {
3758
3818
  requireEmail(email);
3759
3819
  requireOtpCode(code);
3760
- const s = getState();
3761
- const sessionToken = s.identity.getSessionToken();
3762
- if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3763
- return postLink(s, {
3820
+ return postLink(getState(), {
3764
3821
  provider: "email_otp",
3765
3822
  email,
3766
- code,
3767
- session_token: sessionToken
3823
+ code
3768
3824
  });
3769
3825
  },
3770
3826
  linkEmailPassword: async (email, password) => {
3771
3827
  requireEmail(email);
3772
3828
  requirePassword(password);
3773
- const s = getState();
3774
- const sessionToken = s.identity.getSessionToken();
3775
- if (!sessionToken) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3776
- return postLink(s, {
3829
+ return postLink(getState(), {
3777
3830
  provider: "email_password",
3778
3831
  email,
3779
- password,
3780
- session_token: sessionToken
3832
+ password
3781
3833
  });
3782
3834
  },
3783
3835
  unlinkSmsOtp: async () => unlinkProvider("sms_otp"),
@@ -3786,6 +3838,7 @@ const Amba = {
3786
3838
  unlinkGoogle: async () => unlinkProvider("google"),
3787
3839
  getCurrentIdentifiers: async () => {
3788
3840
  const s = getState();
3841
+ await settleSessionInFlight(s);
3789
3842
  if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3790
3843
  return (await s.http.get(P.authIdentifiers)).data.identifiers;
3791
3844
  },
@@ -3812,13 +3865,20 @@ const Amba = {
3812
3865
  refresh: async () => refreshSession(getState()),
3813
3866
  me: async () => {
3814
3867
  const s = getState();
3868
+ await settleSessionInFlight(s);
3815
3869
  if (!s.identity.getSessionToken()) throw new AmbaApiError("UNAUTHORIZED", "no active session — call signIn* first");
3816
3870
  return normalizeUserAuthProviders((await s.http.get(P.authMe)).data);
3817
3871
  },
3818
3872
  getSession: async () => {
3819
3873
  const s = getState();
3874
+ await settleSessionInFlight(s);
3820
3875
  if (!s.identity.isAuthenticated()) return null;
3821
- return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
3876
+ try {
3877
+ return snapshotSession(normalizeUserAuthProviders((await s.http.get(P.authMe)).data));
3878
+ } catch (err) {
3879
+ if (isSessionRefusedError(err) && !s.identity.isAuthenticated()) return null;
3880
+ throw err;
3881
+ }
3822
3882
  },
3823
3883
  getAnonymousId: async () => {
3824
3884
  return getState().identity.getAnonymousId();