@gusnips/react 0.9.1 → 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.
@@ -183,6 +186,24 @@ const next = safeInternalPath(new URLSearchParams(location.search).get("next"))
183
186
 
184
187
  `createRequireAnonymous` already performs that check and falls back to the home path you gave it.
185
188
 
189
+ Writing a return target down is the other direction, and it has its own hazard. A Supabase client
190
+ with no `flowType` is on the **implicit** flow — the default — so a callback comes back as
191
+ `#access_token=…&refresh_token=…`. A guard renders exactly when there is no session yet, which is
192
+ that callback's own state while the client parses the fragment: point an OAuth `redirectTo` at a
193
+ guarded route and a concatenated `pathname + search + hash` puts a refresh token into a query
194
+ string, where it rides in `Referer`, lands in access logs and stays in history.
195
+
196
+ ```ts
197
+ import { returnPathFromLocation } from "@gusnips/react";
198
+
199
+ // Takes anything shaped like the current page: `window.location`, or `useLocation()`.
200
+ const returnTo = returnPathFromLocation(window.location);
201
+ ```
202
+
203
+ It keeps the page and drops only a fragment carrying a credential, because the page is the part
204
+ worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was
205
+ — a dead-session redirect is the common one.
206
+
186
207
  ## Never dead-end anyone
187
208
 
188
209
  ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC;;;;;;;;;;;;;GAaG;AAEH,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,OAAO,CAAC,GAAG,IACrB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAAC;AAajG,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,OAAO,EAAE,SAAS,CAAC;CACpB;AAED,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAOxC;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,YAAY,EAC9B,UAAU,EAAE,MAAM,EAClB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEG,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAalE;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,YAAY,EAC9B,QAAQ,EAAE,MAAM,EAChB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEQ,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAUvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,YAAY,IAE1F,OAAO,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,EAC3B,UAAU,SAAS,GAAG,CAAC,MAAM,SAAS,CAAC,EACvC,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAEP,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAQvE"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC;;;;;;;;;;;;;GAaG;AAEH,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,OAAO,CAAC,GAAG,IACrB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAAC;AAajG,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,OAAO,EAAE,SAAS,CAAC;CACpB;AAED,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAOxC;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,YAAY,EAC9B,UAAU,EAAE,MAAM,EAClB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEG,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAiBlE;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,YAAY,EAC9B,QAAQ,EAAE,MAAM,EAChB,EAAE,OAAO,EAAE,EAAE,YAAY,IAEQ,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAUvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,YAAY,IAE1F,OAAO,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,EAC3B,UAAU,SAAS,GAAG,CAAC,MAAM,SAAS,CAAC,EACvC,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAEP,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAQvE"}
package/dist/guards.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Navigate, useLocation } from "react-router-dom";
3
- import { safeInternalPath } from "./internal-path.js";
3
+ import { returnPathFromLocation, safeInternalPath } from "./internal-path.js";
4
4
  /** Query parameter carrying the route a sign-in interrupted. */
5
5
  export const AUTH_RETURN_PARAM = "next";
6
6
  function signInUrl(signInPath, returnTo) {
@@ -20,8 +20,12 @@ export function createRequireAuth(useSession, signInPath, { loading }) {
20
20
  if (!isAuthenticated) {
21
21
  // Query state survives a reload, an OAuth round trip and an email-link round trip. Router
22
22
  // state survives none of them, so it only looked like a remembered destination on the
23
- // password path. The hash belongs too: tabs and anchored settings are real destinations.
24
- const returnTo = location.pathname + location.search + location.hash;
23
+ // password path. The hash belongs too: tabs and anchored settings are real destinations —
24
+ // but not when it is the round trip itself, which is why this is not a concatenation. This
25
+ // guard renders exactly when there is no session yet, and that is the state an implicit-flow
26
+ // callback is in while its client parses the fragment: point an OAuth `redirectTo` at a
27
+ // guarded route and the raw hash is a refresh token, on its way into a query string.
28
+ const returnTo = returnPathFromLocation(location);
25
29
  return _jsx(Navigate, { to: signInUrl(signInPath, returnTo), replace: true });
26
30
  }
27
31
  return _jsx(_Fragment, { children: children });
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.tsx"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { cn } from "./cn.ts";
2
- export { safeInternalPath } from "./internal-path.ts";
2
+ export { returnPathFromLocation, safeInternalPath, type PathParts } from "./internal-path.ts";
3
3
  export { ApiError, isAbortError, retryAfterSecs } from "./api-error.ts";
4
4
  export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshResult, type RequestOptions, type SessionAdapter, } from "./api-client.ts";
5
5
  export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@
28
28
  // Check that claim against `dist/`, never against this file: a type-only re-export looks
29
29
  // identical in source.
30
30
  export { cn } from "./cn.js";
31
- export { safeInternalPath } from "./internal-path.js";
31
+ export { returnPathFromLocation, safeInternalPath } from "./internal-path.js";
32
32
  export { ApiError, isAbortError, retryAfterSecs } from "./api-error.js";
33
33
  export { createApiClient, } from "./api-client.js";
34
34
  export { createSseParser, readSseStream } from "./sse.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAQb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAQb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
@@ -7,4 +7,25 @@
7
7
  * Parsing against a fixed origin applies the browser's own rules rather than trying to copy them.
8
8
  */
9
9
  export declare function safeInternalPath(value: unknown): string | null;
10
+ /** Anything shaped like the current page: `window.location` and react-router's `useLocation()`. */
11
+ export interface PathParts {
12
+ pathname: string;
13
+ search: string;
14
+ hash: string;
15
+ }
16
+ /**
17
+ * Where someone is right now, written down so signing in can bring them back.
18
+ *
19
+ * The fragment is the half that is easy to get wrong, and getting it wrong leaks a credential. A
20
+ * Supabase client with no `flowType` uses the IMPLICIT flow, which returns the session in the
21
+ * fragment — `#access_token=…&refresh_token=…`. A return target is recorded in a query string, and
22
+ * a query string is not a fragment: it rides in `Referer`, it is written to every access log on the
23
+ * way, and it stays in history. So a fragment carrying a credential is dropped and the page is
24
+ * kept, because the page is the part worth returning to and the anchor is the part that is
25
+ * dangerous.
26
+ *
27
+ * Total on purpose — the caller is handing over its OWN address, which is on this origin by
28
+ * construction. Use `safeInternalPath` for the other direction, where someone else wrote the value.
29
+ */
30
+ export declare function returnPathFromLocation({ pathname, search, hash }: PathParts): string;
10
31
  //# sourceMappingURL=internal-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-path.d.ts","sourceRoot":"","sources":["../src/internal-path.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU9D"}
1
+ {"version":3,"file":"internal-path.d.ts","sourceRoot":"","sources":["../src/internal-path.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU9D;AAeD,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,SAAS,GAAG,MAAM,CAKpF"}
@@ -28,4 +28,36 @@ export function safeInternalPath(value) {
28
28
  return null;
29
29
  }
30
30
  }
31
+ /**
32
+ * The keys an OAuth or OIDC implicit response puts in the fragment that are themselves credentials
33
+ * (RFC 6749 §4.2.2, plus the two a provider token is passed through under). The rest of that
34
+ * response — `expires_in`, `token_type`, `state`, `type` — is metadata, worthless on its own.
35
+ */
36
+ const CREDENTIAL_FRAGMENT_KEYS = [
37
+ "access_token",
38
+ "refresh_token",
39
+ "id_token",
40
+ "provider_token",
41
+ "provider_refresh_token",
42
+ ];
43
+ /**
44
+ * Where someone is right now, written down so signing in can bring them back.
45
+ *
46
+ * The fragment is the half that is easy to get wrong, and getting it wrong leaks a credential. A
47
+ * Supabase client with no `flowType` uses the IMPLICIT flow, which returns the session in the
48
+ * fragment — `#access_token=…&refresh_token=…`. A return target is recorded in a query string, and
49
+ * a query string is not a fragment: it rides in `Referer`, it is written to every access log on the
50
+ * way, and it stays in history. So a fragment carrying a credential is dropped and the page is
51
+ * kept, because the page is the part worth returning to and the anchor is the part that is
52
+ * dangerous.
53
+ *
54
+ * Total on purpose — the caller is handing over its OWN address, which is on this origin by
55
+ * construction. Use `safeInternalPath` for the other direction, where someone else wrote the value.
56
+ */
57
+ export function returnPathFromLocation({ pathname, search, hash }) {
58
+ const fragment = hash.startsWith("#") ? hash.slice(1) : hash;
59
+ const params = new URLSearchParams(fragment);
60
+ const carriesCredential = CREDENTIAL_FRAGMENT_KEYS.some((key) => params.has(key));
61
+ return `${pathname}${search}${carriesCredential ? "" : hash}`;
62
+ }
31
63
  //# sourceMappingURL=internal-path.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-path.js","sourceRoot":"","sources":["../src/internal-path.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD,SAAS,eAAe,CAAC,KAAa;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE/F,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QAC5C,IAAI,GAAG,CAAC,MAAM,KAAK,eAAe;YAAE,OAAO,IAAI,CAAC;QAChD,OAAO,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"internal-path.js","sourceRoot":"","sources":["../src/internal-path.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD,SAAS,eAAe,CAAC,KAAa;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE/F,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QAC5C,IAAI,GAAG,CAAC,MAAM,KAAK,eAAe;YAAE,OAAO,IAAI,CAAC;QAChD,OAAO,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,wBAAwB,GAAG;IAC/B,cAAc;IACd,eAAe;IACf,UAAU;IACV,gBAAgB;IAChB,wBAAwB;CACzB,CAAC;AASF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAa;IAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAClF,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAChE,CAAC"}
@@ -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.1",
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",
@@ -105,6 +105,21 @@ describe("createRequireAuth", () => {
105
105
  });
106
106
  });
107
107
 
108
+ // Point an OAuth `redirectTo` at a guarded route and this is the first render after the provider
109
+ // returns: no session yet, and the implicit flow's tokens still sitting in the address bar. The
110
+ // page is worth remembering; the fragment would be a refresh token written into a query string.
111
+ it("refuses to carry a callback's tokens into the sign-in URL", () => {
112
+ router.useLocation.mockReturnValueOnce({
113
+ pathname: "/dashboard",
114
+ search: "?tab=usage",
115
+ hash: "#access_token=eyJhbG.p.s&refresh_token=v1_abc&token_type=bearer",
116
+ });
117
+ expect(branch(render(guard(false)))).toEqual({
118
+ to: "/login?next=%2Fdashboard%3Ftab%3Dusage",
119
+ state: undefined,
120
+ });
121
+ });
122
+
108
123
  it("lets a signed-in visitor through", () => {
109
124
  expect(branch(render(guard(true)))).toBe("CONTENT");
110
125
  });
package/src/guards.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { UseQueryResult } from "@tanstack/react-query";
2
2
  import type { ReactNode } from "react";
3
3
  import { Navigate, useLocation } from "react-router-dom";
4
- import { safeInternalPath } from "./internal-path.ts";
4
+ import { returnPathFromLocation, safeInternalPath } from "./internal-path.ts";
5
5
 
6
6
  /**
7
7
  * Route guards.
@@ -83,8 +83,12 @@ export function createRequireAuth(
83
83
  if (!isAuthenticated) {
84
84
  // Query state survives a reload, an OAuth round trip and an email-link round trip. Router
85
85
  // state survives none of them, so it only looked like a remembered destination on the
86
- // password path. The hash belongs too: tabs and anchored settings are real destinations.
87
- const returnTo = location.pathname + location.search + location.hash;
86
+ // password path. The hash belongs too: tabs and anchored settings are real destinations —
87
+ // but not when it is the round trip itself, which is why this is not a concatenation. This
88
+ // guard renders exactly when there is no session yet, and that is the state an implicit-flow
89
+ // callback is in while its client parses the fragment: point an OAuth `redirectTo` at a
90
+ // guarded route and the raw hash is a refresh token, on its way into a query string.
91
+ const returnTo = returnPathFromLocation(location);
88
92
  return <Navigate to={signInUrl(signInPath, returnTo)} replace />;
89
93
  }
90
94
  return <>{children}</>;
package/src/index.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  // identical in source.
30
30
 
31
31
  export { cn } from "./cn.ts";
32
- export { safeInternalPath } from "./internal-path.ts";
32
+ export { returnPathFromLocation, safeInternalPath, type PathParts } from "./internal-path.ts";
33
33
 
34
34
  export { ApiError, isAbortError, retryAfterSecs } from "./api-error.ts";
35
35
  export {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { safeInternalPath } from "./internal-path.ts";
2
+ import { returnPathFromLocation, safeInternalPath } from "./internal-path.ts";
3
3
 
4
4
  describe("safeInternalPath", () => {
5
5
  it.each([
@@ -26,3 +26,48 @@ describe("safeInternalPath", () => {
26
26
  expect(safeInternalPath(value)).toBeNull();
27
27
  });
28
28
  });
29
+
30
+ describe("returnPathFromLocation", () => {
31
+ it("keeps the whole address, fragment included", () => {
32
+ expect(
33
+ returnPathFromLocation({
34
+ pathname: "/w/acme/products/api/changes",
35
+ search: "?feature=f1",
36
+ hash: "#change-8f3",
37
+ }),
38
+ ).toBe("/w/acme/products/api/changes?feature=f1#change-8f3");
39
+ });
40
+
41
+ it.each([
42
+ "access_token",
43
+ "refresh_token",
44
+ "id_token",
45
+ "provider_token",
46
+ "provider_refresh_token",
47
+ ])("drops a fragment carrying %s, and keeps the page", (key) => {
48
+ expect(
49
+ returnPathFromLocation({
50
+ pathname: "/app",
51
+ search: "?tab=repos",
52
+ hash: `#${key}=ey.J.h&expires_in=3600&token_type=bearer`,
53
+ }),
54
+ ).toBe("/app?tab=repos");
55
+ });
56
+
57
+ it("drops the whole implicit response, not only its credential keys", () => {
58
+ // Verbatim shape of what a Supabase client with no `flowType` puts back in the address bar.
59
+ const hash =
60
+ "#access_token=eyJhbG.payload.sig&expires_at=1790000000&expires_in=3600" +
61
+ "&provider_token=gho_x&refresh_token=v1_abc&token_type=bearer&type=magiclink";
62
+ expect(returnPathFromLocation({ pathname: "/app", search: "", hash })).toBe("/app");
63
+ });
64
+
65
+ it("keeps a fragment that only looks like parameters", () => {
66
+ const at = { pathname: "/docs", search: "", hash: "#tab=repos&expires_in=3600" };
67
+ expect(returnPathFromLocation(at)).toBe("/docs#tab=repos&expires_in=3600");
68
+ });
69
+
70
+ it("handles an address with no fragment at all", () => {
71
+ expect(returnPathFromLocation({ pathname: "/", search: "", hash: "" })).toBe("/");
72
+ });
73
+ });
@@ -27,3 +27,44 @@ export function safeInternalPath(value: unknown): string | null {
27
27
  return null;
28
28
  }
29
29
  }
30
+
31
+ /**
32
+ * The keys an OAuth or OIDC implicit response puts in the fragment that are themselves credentials
33
+ * (RFC 6749 §4.2.2, plus the two a provider token is passed through under). The rest of that
34
+ * response — `expires_in`, `token_type`, `state`, `type` — is metadata, worthless on its own.
35
+ */
36
+ const CREDENTIAL_FRAGMENT_KEYS = [
37
+ "access_token",
38
+ "refresh_token",
39
+ "id_token",
40
+ "provider_token",
41
+ "provider_refresh_token",
42
+ ];
43
+
44
+ /** Anything shaped like the current page: `window.location` and react-router's `useLocation()`. */
45
+ export interface PathParts {
46
+ pathname: string;
47
+ search: string;
48
+ hash: string;
49
+ }
50
+
51
+ /**
52
+ * Where someone is right now, written down so signing in can bring them back.
53
+ *
54
+ * The fragment is the half that is easy to get wrong, and getting it wrong leaks a credential. A
55
+ * Supabase client with no `flowType` uses the IMPLICIT flow, which returns the session in the
56
+ * fragment — `#access_token=…&refresh_token=…`. A return target is recorded in a query string, and
57
+ * a query string is not a fragment: it rides in `Referer`, it is written to every access log on the
58
+ * way, and it stays in history. So a fragment carrying a credential is dropped and the page is
59
+ * kept, because the page is the part worth returning to and the anchor is the part that is
60
+ * dangerous.
61
+ *
62
+ * Total on purpose — the caller is handing over its OWN address, which is on this origin by
63
+ * construction. Use `safeInternalPath` for the other direction, where someone else wrote the value.
64
+ */
65
+ export function returnPathFromLocation({ pathname, search, hash }: PathParts): string {
66
+ const fragment = hash.startsWith("#") ? hash.slice(1) : hash;
67
+ const params = new URLSearchParams(fragment);
68
+ const carriesCredential = CREDENTIAL_FRAGMENT_KEYS.some((key) => params.has(key));
69
+ return `${pathname}${search}${carriesCredential ? "" : hash}`;
70
+ }
@@ -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 {