@gusnips/react 0.9.2 → 0.9.3

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
@@ -34,9 +34,11 @@ token when you use it, so the losers of that race each invalidate the winner —
34
34
  signed out in the middle of a load that was working. Three separate codebases arrived at
35
35
  single-flight refresh independently. This is that.
36
36
 
37
- **A refresh that never reached the server is not a "no".** Dropping a packet tells you nothing
38
- about whether a session is good. So `refresh()` answers `{ token, reachedAuth }`, and only a real
39
- refusal signs anyone out. Before that split, a Wi-Fi blip logged people out mid-load.
37
+ **Only auth ANSWERING is a "no".** Dropping a packet tells you nothing about whether a session is
38
+ good, and neither does a 500 — that is auth failing, not auth answering. So `refresh()` answers
39
+ `{ token, reachedAuth }`, and only a real refusal signs anyone out. Before that split, a Wi-Fi
40
+ blip logged people out mid-load; without the second half, one bad minute at the auth server signs
41
+ out everybody whose token happened to need refreshing.
40
42
 
41
43
  **A sign-out has a fail-safe timer.** Awaiting `signOut()` before redirecting covers a rejection,
42
44
  not a hang — and a hang leaves someone signed out in name only: every request 401ing, nothing
@@ -63,8 +65,9 @@ Supabase email links have two complete patterns. Keep either one, never half of
63
65
  implicit `#access_token` or a PKCE `?code`; it does not consume `token_hash`.
64
66
 
65
67
  Recovery and invite links continue to the new-password screen after the session opens. A callback
66
- must also tell a used or expired link apart from a request that never reached GoTrue: the first
67
- needs a new link, while the second can retry the same one.
68
+ must also tell a used or expired link apart from a request that never landed — or one that landed
69
+ and came back 5xx: the first needs a new link, and the other two can retry the same one, because
70
+ the hash was never spent.
68
71
 
69
72
  There is also a deadline on every request, which none of the codebases this came from had. A
70
73
  request with no timeout is a spinner with no end.
@@ -19,9 +19,10 @@ export interface SupabaseSessionAuth {
19
19
  /**
20
20
  * Connects a Supabase auth client to {@link SessionAdapter}.
21
21
  *
22
- * The distinction this preserves is the one that decides whether anybody is signed out: a
23
- * retryable fetch error never reached GoTrue and says nothing about the session; any other auth
24
- * error is an answer. A thrown network error stays on the first side of that line too.
22
+ * The distinction this preserves is the one that decides whether anybody is signed out: only
23
+ * auth ANSWERING proves a session is gone. A fetch that never landed says nothing about it, and
24
+ * neither does a 5xx, which is auth failing rather than auth answering. A thrown network error
25
+ * stays on that side of the line too.
25
26
  */
26
27
  export declare function createSupabaseSessionAdapter(auth: SupabaseSessionAuth): SessionAdapter;
27
28
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"supabase-session.d.ts","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,UAAU,qBAAqB;IAC7B,IAAI,EAAE;QAAE,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD;AAED,8FAA8F;AAC9F,MAAM,WAAW,mBAAmB;IAClC,UAAU,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7C,cAAc,IAAI,OAAO,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,OAAO,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxC;AAcD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,mBAAmB,GAAG,cAAc,CAsBtF"}
1
+ {"version":3,"file":"supabase-session.d.ts","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,UAAU,qBAAqB;IAC7B,IAAI,EAAE;QAAE,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD;AAED,8FAA8F;AAC9F,MAAM,WAAW,mBAAmB;IAClC,UAAU,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7C,cAAc,IAAI,OAAO,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,OAAO,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxC;AAsBD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,mBAAmB,GAAG,cAAc,CAsBtF"}
@@ -2,6 +2,15 @@ import { isAuthError, isAuthRetryableFetchError } from "@supabase/supabase-js";
2
2
  function reachedAuth(error) {
3
3
  if (!isAuthError(error) || isAuthRetryableFetchError(error))
4
4
  return false;
5
+ // A 5xx is auth failing, not auth answering — and the vendor's predicate above cannot be
6
+ // relied on to say so, because it reads a list it owns and has already changed: at auth-js
7
+ // 2.91 it covered 502, 503 and 504, and at 2.108 it covers 500 through 530. Code leaning on
8
+ // it alone is right at whichever version happens to be installed, which is not the same as
9
+ // being right. Without this clause one bad minute at GoTrue signs out everybody whose token
10
+ // happened to need refreshing. Measured across the fleet: four backends answered 401 to
11
+ // their own auth provider's 500 for exactly this reason.
12
+ if ((error.status ?? 0) >= 500)
13
+ return false;
5
14
  // Newer clients name a refresh that reached GoTrue but was deliberately discarded because the
6
15
  // local session changed mid-flight. That race is a no-op, not proof that either session died.
7
16
  return error.name !== "AuthRefreshDiscardedError";
@@ -9,9 +18,10 @@ function reachedAuth(error) {
9
18
  /**
10
19
  * Connects a Supabase auth client to {@link SessionAdapter}.
11
20
  *
12
- * The distinction this preserves is the one that decides whether anybody is signed out: a
13
- * retryable fetch error never reached GoTrue and says nothing about the session; any other auth
14
- * error is an answer. A thrown network error stays on the first side of that line too.
21
+ * The distinction this preserves is the one that decides whether anybody is signed out: only
22
+ * auth ANSWERING proves a session is gone. A fetch that never landed says nothing about it, and
23
+ * neither does a 5xx, which is auth failing rather than auth answering. A thrown network error
24
+ * stays on that side of the line too.
15
25
  */
16
26
  export function createSupabaseSessionAdapter(auth) {
17
27
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"supabase-session.js","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAuB,MAAM,uBAAuB,CAAC;AAmBpG,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1E,8FAA8F;IAC9F,8FAA8F;IAC9F,OAAO,KAAK,CAAC,IAAI,KAAK,2BAA2B,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAyB;IACpE,OAAO;QACL,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpD,OAAO;oBACL,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI;oBACzC,WAAW,EAAE,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC;iBAClD,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"supabase-session.js","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAuB,MAAM,uBAAuB,CAAC;AAmBpG,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1E,yFAAyF;IACzF,2FAA2F;IAC3F,4FAA4F;IAC5F,2FAA2F;IAC3F,4FAA4F;IAC5F,wFAAwF;IACxF,yDAAyD;IACzD,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC;IAC7C,8FAA8F;IAC9F,8FAA8F;IAC9F,OAAO,KAAK,CAAC,IAAI,KAAK,2BAA2B,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAyB;IACpE,OAAO;QACL,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpD,OAAO;oBACL,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI;oBACzC,WAAW,EAAE,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC;iBAClD,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,20 @@ describe("createSupabaseSessionAdapter", () => {
67
67
  await expect(thrown.refresh()).resolves.toEqual({ token: null, reachedAuth: true });
68
68
  });
69
69
 
70
+ it("keeps the session when auth answers with a failure of its own", async () => {
71
+ // `isAuthRetryableFetchError` says false for this one — it is an API error, not a fetch
72
+ // failure — so what holds the session is the status clause beside it. A 5xx is auth
73
+ // failing, never auth answering, and reading it as an answer signs the person out.
74
+ const outage = new AuthApiError("Internal Server Error", 500, undefined);
75
+ const adapter = createSupabaseSessionAdapter(
76
+ auth({
77
+ refreshSession: async () => ({ data: { session: null }, error: outage }),
78
+ }),
79
+ );
80
+
81
+ await expect(adapter.refresh()).resolves.toEqual({ token: null, reachedAuth: false });
82
+ });
83
+
70
84
  it("does not kill a session whose rotated refresh was discarded after a local race", async () => {
71
85
  const adapter = createSupabaseSessionAdapter(
72
86
  auth({
@@ -19,6 +19,14 @@ type _SupabaseAuthFitsAdapter = Satisfied<
19
19
 
20
20
  function reachedAuth(error: unknown): boolean {
21
21
  if (!isAuthError(error) || isAuthRetryableFetchError(error)) return false;
22
+ // A 5xx is auth failing, not auth answering — and the vendor's predicate above cannot be
23
+ // relied on to say so, because it reads a list it owns and has already changed: at auth-js
24
+ // 2.91 it covered 502, 503 and 504, and at 2.108 it covers 500 through 530. Code leaning on
25
+ // it alone is right at whichever version happens to be installed, which is not the same as
26
+ // being right. Without this clause one bad minute at GoTrue signs out everybody whose token
27
+ // happened to need refreshing. Measured across the fleet: four backends answered 401 to
28
+ // their own auth provider's 500 for exactly this reason.
29
+ if ((error.status ?? 0) >= 500) return false;
22
30
  // Newer clients name a refresh that reached GoTrue but was deliberately discarded because the
23
31
  // local session changed mid-flight. That race is a no-op, not proof that either session died.
24
32
  return error.name !== "AuthRefreshDiscardedError";
@@ -27,9 +35,10 @@ function reachedAuth(error: unknown): boolean {
27
35
  /**
28
36
  * Connects a Supabase auth client to {@link SessionAdapter}.
29
37
  *
30
- * The distinction this preserves is the one that decides whether anybody is signed out: a
31
- * retryable fetch error never reached GoTrue and says nothing about the session; any other auth
32
- * error is an answer. A thrown network error stays on the first side of that line too.
38
+ * The distinction this preserves is the one that decides whether anybody is signed out: only
39
+ * auth ANSWERING proves a session is gone. A fetch that never landed says nothing about it, and
40
+ * neither does a 5xx, which is auth failing rather than auth answering. A thrown network error
41
+ * stays on that side of the line too.
33
42
  */
34
43
  export function createSupabaseSessionAdapter(auth: SupabaseSessionAuth): SessionAdapter {
35
44
  return {