@aglyn/shared-util-next 1.0.0-beta.216 → 1.0.0-beta.218
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-util-next",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.218",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"./package.json": "./package.json"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
27
|
+
"@aglyn/shared-util-http": "1.0.0-beta.218",
|
|
28
28
|
"@swc/helpers": "0.5.23"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
var _process_env_NEXT_PUBLIC_WORKSPACE_DOMAIN;
|
|
2
1
|
/**
|
|
3
2
|
* @license
|
|
4
3
|
* Copyright 2024 Aglyn LLC
|
|
@@ -15,6 +14,7 @@ var _process_env_NEXT_PUBLIC_WORKSPACE_DOMAIN;
|
|
|
15
14
|
* See the License for the specific language governing permissions and
|
|
16
15
|
* limitations under the License.
|
|
17
16
|
*/ 'use client';
|
|
17
|
+
var _process_env_NEXT_PUBLIC_WORKSPACE_DOMAIN;
|
|
18
18
|
import { isSameOriginPath } from "@aglyn/shared-util-http/safe-redirect";
|
|
19
19
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
20
20
|
import { useCallback, useMemo } from "react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/next/src/lib/use-continue-url.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2024 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\n\nexport type UseContinueUrlDecodedRoutePusher = (\n url?: string,\n as?: string,\n options?: { shallow?: boolean; locale?: string | false; scroll?: boolean },\n) => void\n\nexport type UseContinueUrlDecodedResponse = [\n decoded: string,\n pushNext: UseContinueUrlDecodedRoutePusher,\n]\n\nexport type UseContinueUrlResponse = [\n encoded: string,\n decoded: string,\n pushNext: UseContinueUrlDecodedRoutePusher,\n]\n\nexport const ContinueParamName = 'continue'\nexport const continueParam = (value: string) => `${ContinueParamName}=${value}`\n\nconst WORKSPACE_DOMAIN =\n process.env.NEXT_PUBLIC_WORKSPACE_DOMAIN ?? 'aglyn.com'\n\n/**\n * Same-site absolute returns (AGL-465): the auth host signs a user in and\n * must redirect back to the {org}.<workspaceDomain> subdomain they started\n * from, which is a cross-origin URL. Allow it only when the host is within\n * the workspace domain over https, so this stays first-party and never\n * becomes an open redirect.\n */\nconst isSameSiteAbsoluteUrl = (url: string): boolean => {\n try {\n const { protocol, hostname } = new URL(url)\n if (protocol !== 'https:') return false\n return (\n hostname === WORKSPACE_DOMAIN ||\n hostname.endsWith(`.${WORKSPACE_DOMAIN}`)\n )\n } catch {\n return false\n }\n}\n\n/**\n * Only same-app relative paths, or same-site absolute URLs within the\n * workspace domain (AGL-465), may be continued to — anything else absolute\n * or protocol-relative would make the post-auth redirect an open redirect.\n *\n * The relative branch defers to {@link isSameOriginPath} rather than testing\n * the string's shape (AGL-1881). AGL-2486 rejected `\\` here because the\n * WHATWG URL parser treats it as `/` in the authority position, so\n * `/\\evil.com` passed a `startsWith('/') && !startsWith('//')` test and still\n * resolved off-site. That was true but incomplete: the same parser also\n * DELETES every tab, LF and CR before parsing, so `/<TAB>/evil.com` resolves\n * to `https://evil.com/` while containing neither `//` nor `\\`. Enumerating\n * the tricks was the mistake; the shared predicate resolves the value and\n * compares origins, which is the question `window.location.assign` will\n * actually ask.\n *\n * The absolute branch below never had that bug and keeps its own check: once\n * the input is absolute the parser's `hostname` is already ground truth.\n *\n * A `continue` that has survived an external IdP round trip is the case that\n * makes this matter: whatever we put into `state` comes back as untrusted\n * input.\n *\n * `''` is rejected explicitly rather than by accident. `strictNullChecks` is\n * off repo-wide, so an absent param arrives here as an empty string far more\n * often than as `undefined`, and \"no continue\" must read as unsafe so the\n * caller falls back to its own default instead of building `?continue=`.\n */\nexport const isSafeContinueUrl = (url: string): boolean => {\n if (!url) return false\n return isSameOriginPath(url) || isSameSiteAbsoluteUrl(url)\n}\n\n/**\n * Forward the continue URL onto another same-app path (AGL-2486).\n *\n * A signed-out user who deep-links in arrives at `/signin?continue=…`, and\n * every link OUT of that page to another auth route — the SSO button above\n * all — has to carry the value or the destination they were going to is\n * gone. Observed on the SSO button: the url does not carry the\n * continue url and is dropped entirely.\n *\n * Unsafe or absent values append nothing, so a poisoned `continue` degrades\n * to a plain link rather than being laundered one hop further along.\n */\nexport function withContinueUrl(path: string, continueUrl: string): string {\n if (!isSafeContinueUrl(continueUrl)) return path\n const separator = path.includes('?') ? '&' : '?'\n return `${path}${separator}${continueParam(encodeURIComponent(continueUrl))}`\n}\n\n/**\n * `href` for a link out of the current page that keeps the continue URL.\n *\n * Deliberately reads `useSearchParams` directly instead of reusing\n * {@link useContinueUrlDecoded}: that hook also calls `useRouter`, and a\n * link does not navigate imperatively. Pulling the router in would make\n * every page that renders one of these links require an App Router context\n * it does not otherwise need.\n */\nexport function useContinueHref(path: string): string {\n const searchParams = useSearchParams()\n const raw = searchParams?.get(ContinueParamName) ?? ''\n return useMemo(() => withContinueUrl(path, raw), [path, raw])\n}\n\nexport function useContinueUrlDecoded(): UseContinueUrlDecodedResponse {\n const router = useRouter()\n // App Router: query strings live in useSearchParams — useParams only\n // carries dynamic route segments, which is why the continue redirect\n // silently broke after the pages→app migration (AGL-458). `get` already\n // percent-decodes, so no second decode (it would corrupt paths that\n // legitimately contain %-sequences).\n const searchParams = useSearchParams()\n\n const continueUrl = useMemo(() => {\n const url = searchParams?.get(ContinueParamName) ?? ''\n return isSafeContinueUrl(url) ? url : ''\n }, [searchParams])\n\n const pushNext = useCallback(\n (\n url = '/',\n as: string | undefined = undefined,\n options?: {\n shallow?: boolean\n locale?: string | false\n scroll?: boolean\n },\n ): void => {\n const target = continueUrl || url\n // A same-site absolute return (AGL-465) is cross-origin — the App\n // Router's client navigation can't cross origins, so hand it to the\n // browser. Relative paths keep the SPA transition.\n if (/^https?:\\/\\//.test(target)) {\n if (typeof window !== 'undefined') window.location.assign(target)\n return\n }\n return router.push(target, options)\n },\n [router, continueUrl],\n )\n\n return useMemo(() => {\n return [continueUrl, pushNext]\n }, [continueUrl, pushNext])\n}\n\nexport function useContinueUrlEncoded() {\n const pathname = usePathname()\n\n return useMemo(() => {\n return encodeURIComponent(pathname || '')\n }, [pathname])\n}\n\nexport function useContinueUrl(): UseContinueUrlResponse {\n const encoded = useContinueUrlEncoded()\n const [decoded, pushNext] = useContinueUrlDecoded()\n\n return useMemo(\n () => [encoded, decoded, pushNext],\n [encoded, decoded, pushNext],\n )\n}\n\nexport default useContinueUrl\n"],"names":["process","isSameOriginPath","usePathname","useRouter","useSearchParams","useCallback","useMemo","ContinueParamName","continueParam","value","WORKSPACE_DOMAIN","env","NEXT_PUBLIC_WORKSPACE_DOMAIN","isSameSiteAbsoluteUrl","url","protocol","hostname","URL","endsWith","isSafeContinueUrl","withContinueUrl","path","continueUrl","separator","includes","encodeURIComponent","useContinueHref","searchParams","raw","get","useContinueUrlDecoded","router","pushNext","as","undefined","options","target","test","window","location","assign","push","useContinueUrlEncoded","pathname","useContinueUrl","encoded","decoded"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/next/src/lib/use-continue-url.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2024 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { isSameOriginPath } from '@aglyn/shared-util-http/safe-redirect'\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\n\nexport type UseContinueUrlDecodedRoutePusher = (\n url?: string,\n as?: string,\n options?: { shallow?: boolean; locale?: string | false; scroll?: boolean },\n) => void\n\nexport type UseContinueUrlDecodedResponse = [\n decoded: string,\n pushNext: UseContinueUrlDecodedRoutePusher,\n]\n\nexport type UseContinueUrlResponse = [\n encoded: string,\n decoded: string,\n pushNext: UseContinueUrlDecodedRoutePusher,\n]\n\nexport const ContinueParamName = 'continue'\nexport const continueParam = (value: string) => `${ContinueParamName}=${value}`\n\nconst WORKSPACE_DOMAIN =\n process.env.NEXT_PUBLIC_WORKSPACE_DOMAIN ?? 'aglyn.com'\n\n/**\n * Same-site absolute returns (AGL-465): the auth host signs a user in and\n * must redirect back to the {org}.<workspaceDomain> subdomain they started\n * from, which is a cross-origin URL. Allow it only when the host is within\n * the workspace domain over https, so this stays first-party and never\n * becomes an open redirect.\n */\nconst isSameSiteAbsoluteUrl = (url: string): boolean => {\n try {\n const { protocol, hostname } = new URL(url)\n if (protocol !== 'https:') return false\n return (\n hostname === WORKSPACE_DOMAIN ||\n hostname.endsWith(`.${WORKSPACE_DOMAIN}`)\n )\n } catch {\n return false\n }\n}\n\n/**\n * Only same-app relative paths, or same-site absolute URLs within the\n * workspace domain (AGL-465), may be continued to — anything else absolute\n * or protocol-relative would make the post-auth redirect an open redirect.\n *\n * The relative branch defers to {@link isSameOriginPath} rather than testing\n * the string's shape (AGL-1881). AGL-2486 rejected `\\` here because the\n * WHATWG URL parser treats it as `/` in the authority position, so\n * `/\\evil.com` passed a `startsWith('/') && !startsWith('//')` test and still\n * resolved off-site. That was true but incomplete: the same parser also\n * DELETES every tab, LF and CR before parsing, so `/<TAB>/evil.com` resolves\n * to `https://evil.com/` while containing neither `//` nor `\\`. Enumerating\n * the tricks was the mistake; the shared predicate resolves the value and\n * compares origins, which is the question `window.location.assign` will\n * actually ask.\n *\n * The absolute branch below never had that bug and keeps its own check: once\n * the input is absolute the parser's `hostname` is already ground truth.\n *\n * A `continue` that has survived an external IdP round trip is the case that\n * makes this matter: whatever we put into `state` comes back as untrusted\n * input.\n *\n * `''` is rejected explicitly rather than by accident. `strictNullChecks` is\n * off repo-wide, so an absent param arrives here as an empty string far more\n * often than as `undefined`, and \"no continue\" must read as unsafe so the\n * caller falls back to its own default instead of building `?continue=`.\n */\nexport const isSafeContinueUrl = (url: string): boolean => {\n if (!url) return false\n return isSameOriginPath(url) || isSameSiteAbsoluteUrl(url)\n}\n\n/**\n * Forward the continue URL onto another same-app path (AGL-2486).\n *\n * A signed-out user who deep-links in arrives at `/signin?continue=…`, and\n * every link OUT of that page to another auth route — the SSO button above\n * all — has to carry the value or the destination they were going to is\n * gone. Observed on the SSO button: the url does not carry the\n * continue url and is dropped entirely.\n *\n * Unsafe or absent values append nothing, so a poisoned `continue` degrades\n * to a plain link rather than being laundered one hop further along.\n */\nexport function withContinueUrl(path: string, continueUrl: string): string {\n if (!isSafeContinueUrl(continueUrl)) return path\n const separator = path.includes('?') ? '&' : '?'\n return `${path}${separator}${continueParam(encodeURIComponent(continueUrl))}`\n}\n\n/**\n * `href` for a link out of the current page that keeps the continue URL.\n *\n * Deliberately reads `useSearchParams` directly instead of reusing\n * {@link useContinueUrlDecoded}: that hook also calls `useRouter`, and a\n * link does not navigate imperatively. Pulling the router in would make\n * every page that renders one of these links require an App Router context\n * it does not otherwise need.\n */\nexport function useContinueHref(path: string): string {\n const searchParams = useSearchParams()\n const raw = searchParams?.get(ContinueParamName) ?? ''\n return useMemo(() => withContinueUrl(path, raw), [path, raw])\n}\n\nexport function useContinueUrlDecoded(): UseContinueUrlDecodedResponse {\n const router = useRouter()\n // App Router: query strings live in useSearchParams — useParams only\n // carries dynamic route segments, which is why the continue redirect\n // silently broke after the pages→app migration (AGL-458). `get` already\n // percent-decodes, so no second decode (it would corrupt paths that\n // legitimately contain %-sequences).\n const searchParams = useSearchParams()\n\n const continueUrl = useMemo(() => {\n const url = searchParams?.get(ContinueParamName) ?? ''\n return isSafeContinueUrl(url) ? url : ''\n }, [searchParams])\n\n const pushNext = useCallback(\n (\n url = '/',\n as: string | undefined = undefined,\n options?: {\n shallow?: boolean\n locale?: string | false\n scroll?: boolean\n },\n ): void => {\n const target = continueUrl || url\n // A same-site absolute return (AGL-465) is cross-origin — the App\n // Router's client navigation can't cross origins, so hand it to the\n // browser. Relative paths keep the SPA transition.\n if (/^https?:\\/\\//.test(target)) {\n if (typeof window !== 'undefined') window.location.assign(target)\n return\n }\n return router.push(target, options)\n },\n [router, continueUrl],\n )\n\n return useMemo(() => {\n return [continueUrl, pushNext]\n }, [continueUrl, pushNext])\n}\n\nexport function useContinueUrlEncoded() {\n const pathname = usePathname()\n\n return useMemo(() => {\n return encodeURIComponent(pathname || '')\n }, [pathname])\n}\n\nexport function useContinueUrl(): UseContinueUrlResponse {\n const encoded = useContinueUrlEncoded()\n const [decoded, pushNext] = useContinueUrlDecoded()\n\n return useMemo(\n () => [encoded, decoded, pushNext],\n [encoded, decoded, pushNext],\n )\n}\n\nexport default useContinueUrl\n"],"names":["process","isSameOriginPath","usePathname","useRouter","useSearchParams","useCallback","useMemo","ContinueParamName","continueParam","value","WORKSPACE_DOMAIN","env","NEXT_PUBLIC_WORKSPACE_DOMAIN","isSameSiteAbsoluteUrl","url","protocol","hostname","URL","endsWith","isSafeContinueUrl","withContinueUrl","path","continueUrl","separator","includes","encodeURIComponent","useContinueHref","searchParams","raw","get","useContinueUrlDecoded","router","pushNext","as","undefined","options","target","test","window","location","assign","push","useContinueUrlEncoded","pathname","useContinueUrl","encoded","decoded"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;IA2BEA;AAzBF,SAASC,gBAAgB,QAAQ,wCAAuC;AACxE,SAASC,WAAW,EAAEC,SAAS,EAAEC,eAAe,QAAQ,kBAAiB;AACzE,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAO;AAmB5C,OAAO,MAAMC,oBAAoB,WAAU;AAC3C,OAAO,MAAMC,gBAAgB,CAACC,QAAkB,GAAGF,kBAAkB,CAAC,EAAEE,OAAO,CAAA;AAE/E,MAAMC,oBACJV,4CAAAA,QAAQW,GAAG,CAACC,4BAA4B,YAAxCZ,4CAA4C;AAE9C;;;;;;CAMC,GACD,MAAMa,wBAAwB,CAACC;IAC7B,IAAI;QACF,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAG,IAAIC,IAAIH;QACvC,IAAIC,aAAa,UAAU,OAAO;QAClC,OACEC,aAAaN,oBACbM,SAASE,QAAQ,CAAC,CAAC,CAAC,EAAER,kBAAkB;IAE5C,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMS,oBAAoB,CAACL;IAChC,IAAI,CAACA,KAAK,OAAO;IACjB,OAAOb,iBAAiBa,QAAQD,sBAAsBC;AACxD,EAAC;AAED;;;;;;;;;;;CAWC,GACD,OAAO,SAASM,gBAAgBC,IAAY,EAAEC,WAAmB;IAC/D,IAAI,CAACH,kBAAkBG,cAAc,OAAOD;IAC5C,MAAME,YAAYF,KAAKG,QAAQ,CAAC,OAAO,MAAM;IAC7C,OAAO,GAAGH,OAAOE,YAAYf,cAAciB,mBAAmBH,eAAe;AAC/E;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,gBAAgBL,IAAY;;IAC1C,MAAMM,eAAevB;IACrB,MAAMwB,cAAMD,gCAAAA,aAAcE,GAAG,CAACtB,qCAAsB;IACpD,OAAOD,QAAQ,IAAMc,gBAAgBC,MAAMO,MAAM;QAACP;QAAMO;KAAI;AAC9D;AAEA,OAAO,SAASE;IACd,MAAMC,SAAS5B;IACf,qEAAqE;IACrE,qEAAqE;IACrE,wEAAwE;IACxE,oEAAoE;IACpE,qCAAqC;IACrC,MAAMwB,eAAevB;IAErB,MAAMkB,cAAchB,QAAQ;;QAC1B,MAAMQ,cAAMa,gCAAAA,aAAcE,GAAG,CAACtB,qCAAsB;QACpD,OAAOY,kBAAkBL,OAAOA,MAAM;IACxC,GAAG;QAACa;KAAa;IAEjB,MAAMK,WAAW3B,YACf,CACES,MAAM,GAAG,EACTmB,KAAyBC,SAAS,EAClCC;QAMA,MAAMC,SAASd,eAAeR;QAC9B,kEAAkE;QAClE,oEAAoE;QACpE,mDAAmD;QACnD,IAAI,eAAeuB,IAAI,CAACD,SAAS;YAC/B,IAAI,OAAOE,WAAW,aAAaA,OAAOC,QAAQ,CAACC,MAAM,CAACJ;YAC1D;QACF;QACA,OAAOL,OAAOU,IAAI,CAACL,QAAQD;IAC7B,GACA;QAACJ;QAAQT;KAAY;IAGvB,OAAOhB,QAAQ;QACb,OAAO;YAACgB;YAAaU;SAAS;IAChC,GAAG;QAACV;QAAaU;KAAS;AAC5B;AAEA,OAAO,SAASU;IACd,MAAMC,WAAWzC;IAEjB,OAAOI,QAAQ;QACb,OAAOmB,mBAAmBkB,YAAY;IACxC,GAAG;QAACA;KAAS;AACf;AAEA,OAAO,SAASC;IACd,MAAMC,UAAUH;IAChB,MAAM,CAACI,SAASd,SAAS,GAAGF;IAE5B,OAAOxB,QACL,IAAM;YAACuC;YAASC;YAASd;SAAS,EAClC;QAACa;QAASC;QAASd;KAAS;AAEhC;AAEA,eAAeY,eAAc"}
|