@gusnips/react 0.9.1 → 0.9.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/README.md CHANGED
@@ -183,6 +183,24 @@ const next = safeInternalPath(new URLSearchParams(location.search).get("next"))
183
183
 
184
184
  `createRequireAnonymous` already performs that check and falls back to the home path you gave it.
185
185
 
186
+ Writing a return target down is the other direction, and it has its own hazard. A Supabase client
187
+ with no `flowType` is on the **implicit** flow — the default — so a callback comes back as
188
+ `#access_token=…&refresh_token=…`. A guard renders exactly when there is no session yet, which is
189
+ that callback's own state while the client parses the fragment: point an OAuth `redirectTo` at a
190
+ guarded route and a concatenated `pathname + search + hash` puts a refresh token into a query
191
+ string, where it rides in `Referer`, lands in access logs and stays in history.
192
+
193
+ ```ts
194
+ import { returnPathFromLocation } from "@gusnips/react";
195
+
196
+ // Takes anything shaped like the current page: `window.location`, or `useLocation()`.
197
+ const returnTo = returnPathFromLocation(window.location);
198
+ ```
199
+
200
+ It keeps the page and drops only a fragment carrying a credential, because the page is the part
201
+ worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was
202
+ — a dead-session redirect is the common one.
203
+
186
204
  ## Never dead-end anyone
187
205
 
188
206
  ```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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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
+ }