0xdcarns 3 years ago
parent
commit
b610c6f567
100 changed files with 6163 additions and 70 deletions
  1. 11 0
      .github/workflows/buildandrelease.yml
  2. 4 7
      .github/workflows/publish-docker.yml
  3. 8 9
      README.md
  4. 1 1
      auth/auth.go
  5. 3 3
      auth/azure-ad.go
  6. 3 3
      auth/github.go
  7. 3 3
      auth/google.go
  8. 3 6
      compose/docker-compose.caddy.yml
  9. 3 2
      compose/docker-compose.contained.yml
  10. 3 2
      compose/docker-compose.nodns.yml
  11. 3 2
      compose/docker-compose.reference.yml
  12. 3 2
      compose/docker-compose.yml
  13. 2 0
      config/config.go
  14. 48 8
      controllers/dnsHttpController.go
  15. 21 3
      controllers/networkHttpController.go
  16. 19 4
      controllers/nodeGrpcController.go
  17. 1 1
      controllers/relay.go
  18. 26 13
      docker/Dockerfile-netclient-full
  19. 1 1
      docker/Dockerfile-userspace
  20. BIN
      docs/_build/doctrees/about.doctree
  21. BIN
      docs/_build/doctrees/api.doctree
  22. BIN
      docs/_build/doctrees/architecture.doctree
  23. BIN
      docs/_build/doctrees/client-installation.doctree
  24. BIN
      docs/_build/doctrees/conduct.doctree
  25. BIN
      docs/_build/doctrees/environment.pickle
  26. BIN
      docs/_build/doctrees/external-clients.doctree
  27. BIN
      docs/_build/doctrees/getting-started.doctree
  28. BIN
      docs/_build/doctrees/index.doctree
  29. BIN
      docs/_build/doctrees/install.doctree
  30. BIN
      docs/_build/doctrees/license.doctree
  31. BIN
      docs/_build/doctrees/oauth.doctree
  32. BIN
      docs/_build/doctrees/quick-start-nginx.doctree
  33. BIN
      docs/_build/doctrees/quick-start.doctree
  34. BIN
      docs/_build/doctrees/server-installation.doctree
  35. BIN
      docs/_build/doctrees/support.doctree
  36. BIN
      docs/_build/doctrees/troubleshoot.doctree
  37. BIN
      docs/_build/doctrees/usage.doctree
  38. 4 0
      docs/_build/html/.buildinfo
  39. BIN
      docs/_build/html/_images/access-key.png
  40. BIN
      docs/_build/html/_images/create-net.png
  41. BIN
      docs/_build/html/_images/exclient1.png
  42. BIN
      docs/_build/html/_images/exclient2.png
  43. BIN
      docs/_build/html/_images/exclient3.png
  44. BIN
      docs/_build/html/_images/exclient4.png
  45. BIN
      docs/_build/html/_images/extclient5.png
  46. BIN
      docs/_build/html/_images/mesh-diagram.png
  47. BIN
      docs/_build/html/_images/mesh.png
  48. BIN
      docs/_build/html/_images/nc-install-output.png
  49. BIN
      docs/_build/html/_images/netmaker-node.png
  50. BIN
      docs/_build/html/_images/netmaker.png
  51. BIN
      docs/_build/html/_images/nm-diagram-2.jpg
  52. BIN
      docs/_build/html/_images/nm-node-success.png
  53. BIN
      docs/_build/html/_images/node-details.png
  54. BIN
      docs/_build/html/_images/nodes.png
  55. BIN
      docs/_build/html/_images/oauth1.png
  56. BIN
      docs/_build/html/_images/oauth2.png
  57. BIN
      docs/_build/html/_images/oauth3.png
  58. BIN
      docs/_build/html/_images/ping-node.png
  59. 46 0
      docs/_build/html/_sources/about.rst.txt
  60. 184 0
      docs/_build/html/_sources/api.rst.txt
  61. 196 0
      docs/_build/html/_sources/architecture.rst.txt
  62. 160 0
      docs/_build/html/_sources/client-installation.rst.txt
  63. 77 0
      docs/_build/html/_sources/conduct.rst.txt
  64. 72 0
      docs/_build/html/_sources/external-clients.rst.txt
  65. 127 0
      docs/_build/html/_sources/getting-started.rst.txt
  66. 186 0
      docs/_build/html/_sources/index.rst.txt
  67. 20 0
      docs/_build/html/_sources/install.rst.txt
  68. 6 0
      docs/_build/html/_sources/license.rst.txt
  69. 81 0
      docs/_build/html/_sources/oauth.rst.txt
  70. 170 0
      docs/_build/html/_sources/quick-start-nginx.rst.txt
  71. 141 0
      docs/_build/html/_sources/quick-start.rst.txt
  72. 571 0
      docs/_build/html/_sources/server-installation.rst.txt
  73. 61 0
      docs/_build/html/_sources/support.rst.txt
  74. 115 0
      docs/_build/html/_sources/troubleshoot.rst.txt
  75. 24 0
      docs/_build/html/_sources/usage.rst.txt
  76. 861 0
      docs/_build/html/_static/basic.css
  77. 321 0
      docs/_build/html/_static/doctools.js
  78. 12 0
      docs/_build/html/_static/documentation_options.js
  79. BIN
      docs/_build/html/_static/file.png
  80. 3 0
      docs/_build/html/_static/fonts/font-awesome.css
  81. 13 0
      docs/_build/html/_static/fonts/material-icons.css
  82. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.ttf
  83. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.woff
  84. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.woff2
  85. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.ttf
  86. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff
  87. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff2
  88. BIN
      docs/_build/html/_static/images/favicon.png
  89. 1 0
      docs/_build/html/_static/images/icons/bitbucket.1b09e088.svg
  90. 1 0
      docs/_build/html/_static/images/icons/bitbucket.svg
  91. 1 0
      docs/_build/html/_static/images/icons/github.f0b8504a.svg
  92. 1 0
      docs/_build/html/_static/images/icons/github.svg
  93. 1 0
      docs/_build/html/_static/images/icons/gitlab.6dd19c00.svg
  94. 1 0
      docs/_build/html/_static/images/icons/gitlab.svg
  95. 2540 0
      docs/_build/html/_static/javascripts/application.js
  96. 0 0
      docs/_build/html/_static/javascripts/lunr/lunr.da.js
  97. 0 0
      docs/_build/html/_static/javascripts/lunr/lunr.de.js
  98. 0 0
      docs/_build/html/_static/javascripts/lunr/lunr.du.js
  99. 0 0
      docs/_build/html/_static/javascripts/lunr/lunr.es.js
  100. 0 0
      docs/_build/html/_static/javascripts/lunr/lunr.fi.js

+ 11 - 0
.github/workflows/buildandrelease.yml

@@ -39,6 +39,7 @@ jobs:
           env GOOS=linux GOARCH=arm GOARM=6 go build -o build/netclient-arm6/netclient main.go
           env GOOS=linux GOARCH=arm GOARM=7 go build -o build/netclient-arm7/netclient main.go
           env GOOS=linux GOARCH=arm64 go build -o build/netclient-arm64/netclient main.go
+          env GOOS=linux GOARCH=mipsle go build -ldflags "-s -w" -o build/netclient-mipsle/netclient main.go && upx build/netclient-mipsle/netclient
 
       - name: Upload x86 to Release
         uses: svenstaro/upload-release-action@v2
@@ -89,3 +90,13 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-arm64
+
+      - name: Upload mipsle to Release
+        uses: svenstaro/upload-release-action@v2
+        with:
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
+          file: netclient/build/netclient-mipsle/netclient
+          tag: ${{ env.NETMAKER_VERSION }}
+          overwrite: true
+          prerelease: true
+          asset_name: netclient-mipsle

+ 4 - 7
.github/workflows/publish-docker.yml

@@ -6,10 +6,8 @@ on:
       tag:
         description: 'docker tag'
         required: true
-  pull_request:
-    branches:
-      - 'test'
-      - 'master'
+  release:
+    types: [published]
 
 jobs:
   docker:
@@ -31,8 +29,7 @@ jobs:
         uses: docker/setup-qemu-action@v1
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v1
-      - if: github.event_name != 'pull_request'
-        name: Login to DockerHub
+      - name: Login to DockerHub
         uses: docker/login-action@v1
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -42,5 +39,5 @@ jobs:
         with:
           context: .
           platforms: linux/amd64, linux/arm64
-          push: ${{ github.event_name != 'pull_request' }}
+          push: true
           tags: gravitl/netmaker:${{ env.TAG }}

+ 8 - 9
README.md

@@ -8,7 +8,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.8.5-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.9.0-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker" />
@@ -31,33 +31,32 @@
 - [x] Peer-to-Peer Mesh Networks
 - [x] Kubernetes, Multi-Cloud
 - [x] OAuth and Private DNS
-- [x] Linux, Mac, Windows, iPhone, and Android
+- [x] Linux, Mac, Windows, FreeBSD, iPhone, and Android
 
-# Get Started in 5 Minutes
+# Get Started in 5 Minutes  
 
-**For production-grade installations, visit the [Install Docs](https://netmaker.readthedocs.io/en/develop/install.html).**  
+**For DigitalOcean, use the 1-Click App:** <a href="https://marketplace.digitalocean.com/apps/netmaker?refcode=496ffcf1e252"><img src="https://www.deploytodo.com/do-btn-blue.svg" width="15%" /></a>  
+**For production-grade installations, visit the [Install Docs](https://netmaker.readthedocs.io/en/master/install.html).**  
 **For an HA install using helm on k8s, visit the [Helm Repo](https://github.com/gravitl/netmaker-helm/).**
 1. Get a cloud VM with Ubuntu 20.04 and a public IP.
 2. Open ports 443, 53, and 51821-51830/udp on the VM firewall and in cloud security settings.
 3. Run the script **(see below for optional configurations)**:
 
-`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/develop/scripts/nm-quick.sh | bash`
+`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/masters/scripts/nm-quick.sh | bash`
 
 Upon completion, the logs will display a script that can be used to automatically connect Linux and Mac devices. It will also display instructions for Windows, iPhone, and Android.
 
-<img src="./docs/images/install-server.gif" width="50%" /><img src="./docs/images/visit-website.gif" width="50%" />
-
 After installing Netmaker, check out the [Walkthrough](https://itnext.io/getting-started-with-netmaker-a-wireguard-virtual-networking-platform-3d563fbd87f0) and [Getting Started](https://netmaker.readthedocs.io/en/master/getting-started.html) guides to learn more about configuring networks. Or, check out some of our other [Tutorials](https://gravitl.com/resources) for different use cases, including Kubernetes.
 
 ### Optional configurations
 
 **Deploy a "Hub-And-Spoke VPN" on the server**  
 *This will configure a standard VPN (non-meshed) for private internet access, with 10 clients (-c).*  
-`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/develop/scripts/nm-quick.sh | bash -s -- -v true -c 10`  
+`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh | bash -s -- -v true -c 10`  
 
 **Specify Domain and Email**  
 *Make sure your wildcard domain is pointing towards the server ip.*  
-`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/develop/scripts/nm-quick.sh | bash -s -- -d mynetmaker.domain.com -e [email protected]`  
+`sudo wget -qO - https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh | bash -s -- -d mynetmaker.domain.com -e [email protected]`  
 
 **Script Options**  
 ```

+ 1 - 1
auth/auth.go

@@ -93,7 +93,7 @@ func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 	if auth_provider == nil {
 		var referer = r.Header.Get("referer")
 		if referer != "" {
-			http.Redirect(w, r, referer+"?oauth=callback-error", http.StatusTemporaryRedirect)
+			http.Redirect(w, r, referer+"login?oauth=callback-error", http.StatusTemporaryRedirect)
 			return
 		}
 		w.Header().Set("Content-Type", "text/html; charset=utf-8")

+ 3 - 3
auth/azure-ad.go

@@ -42,7 +42,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 	oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?oauth=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	} else if auth_provider == nil {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
@@ -57,7 +57,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getAzureUserInfo(r.FormValue("state"), r.FormValue("code"))
 	if err != nil {
 		logic.Log("error when getting user info from azure: "+err.Error(), 1)
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?oauth=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	}
 	_, err = logic.GetUser(content.UserPrincipalName)
@@ -83,7 +83,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logic.Log("completed azure OAuth sigin in for "+content.UserPrincipalName, 1)
-	http.Redirect(w, r, servercfg.GetFrontendURL()+"?login="+jwt+"&user="+content.UserPrincipalName, http.StatusPermanentRedirect)
+	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.UserPrincipalName, http.StatusPermanentRedirect)
 }
 
 func getAzureUserInfo(state string, code string) (*azureOauthUser, error) {

+ 3 - 3
auth/github.go

@@ -41,7 +41,7 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 	oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?error=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	} else if auth_provider == nil {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
@@ -56,7 +56,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGithubUserInfo(r.URL.Query().Get("state"), r.URL.Query().Get("code"))
 	if err != nil {
 		logic.Log("error when getting user info from github: "+err.Error(), 1)
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?oauth=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	}
 	_, err = logic.GetUser(content.Login)
@@ -82,7 +82,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logic.Log("completed github OAuth sigin in for "+content.Login, 1)
-	http.Redirect(w, r, servercfg.GetFrontendURL()+"?login="+jwt+"&user="+content.Login, http.StatusPermanentRedirect)
+	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Login, http.StatusPermanentRedirect)
 }
 
 func getGithubUserInfo(state string, code string) (*githubOauthUser, error) {

+ 3 - 3
auth/google.go

@@ -41,7 +41,7 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) {
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 	oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?oauth=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	} else if auth_provider == nil {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
@@ -56,7 +56,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGoogleUserInfo(r.FormValue("state"), r.FormValue("code"))
 	if err != nil {
 		logic.Log("error when getting user info from google: "+err.Error(), 1)
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"?oauth=callback-error", http.StatusTemporaryRedirect)
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
 	}
 	_, err = logic.GetUser(content.Email)
@@ -82,7 +82,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logic.Log("completed google OAuth sigin in for "+content.Email, 1)
-	http.Redirect(w, r, servercfg.GetFrontendURL()+"?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
+	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }
 
 func getGoogleUserInfo(state string, code string) (*googleOauthUser, error) {

+ 3 - 6
compose/docker-compose.caddy.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.8.5
+    image: gravitl/netmaker:v0.9.0
     volumes:
       - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
       - /run/systemd/system:/run/systemd/system
@@ -33,17 +33,14 @@ services:
       MASTER_KEY: "REPLACE_MASTER_KEY"
       SERVER_GRPC_WIREGUARD: "off"
       CORS_ALLOWED_ORIGIN: "*"
+      DISPLAY_KEYS: "on"
       DATABASE: "sqlite"
       NODE_ID: "netmaker-server-1"
-    ports:
-      - "51821-51830:51821-51830/udp"
-      - "8081:8081"
-      - "50051:50051"
   netmaker-ui:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.8.5
+    image: gravitl/netmaker-ui:v0.9.0
     links:
       - "netmaker:api"
     ports:

+ 3 - 2
compose/docker-compose.contained.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.8.5
+    image: gravitl/netmaker:v0.9.0
     volumes:
       - dnsconfig:/root/config/dnsconfig
       - /usr/bin/wg:/usr/bin/wg
@@ -27,6 +27,7 @@ services:
       MASTER_KEY: "REPLACE_MASTER_KEY"
       SERVER_GRPC_WIREGUARD: "off"
       CORS_ALLOWED_ORIGIN: "*"
+      DISPLAY_KEYS: "on"
       DATABASE: "sqlite"
       NODE_ID: "netmaker-server-1"
     ports:
@@ -37,7 +38,7 @@ services:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.8.5
+    image: gravitl/netmaker-ui:v0.9.0
     links:
       - "netmaker:api"
     ports:

+ 3 - 2
compose/docker-compose.nodns.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.8.5
+    image: gravitl/netmaker:v0.9.0
     volumes:
       - /usr/bin/wg:/usr/bin/wg
       - sqldata:/root/data
@@ -23,6 +23,7 @@ services:
       API_PORT: "8081"
       GRPC_PORT: "50051"
       CLIENT_MODE: "on"
+      DISPLAY_KEYS: "on"
       MASTER_KEY: "REPLACE_MASTER_KEY"
       SERVER_GRPC_WIREGUARD: "off"
       CORS_ALLOWED_ORIGIN: "*"
@@ -35,7 +36,7 @@ services:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.8.5
+    image: gravitl/netmaker-ui:v0.9.0
     links:
       - "netmaker:api"
     ports:

+ 3 - 2
compose/docker-compose.reference.yml

@@ -11,7 +11,7 @@ services:
     container_name: netmaker
     depends_on:
       - rqlite
-    image: gravitl/netmaker:v0.8.5
+    image: gravitl/netmaker:v0.9.0
     volumes: # Volume mounts necessary for CLIENT_MODE to control wireguard networking on host (except dnsconfig, which is where dns config files are stored for use by CoreDNS)
       - dnsconfig:/root/config/dnsconfig # Netmaker writes Corefile to this location, which gets mounted by CoreDNS for DNS configuration.
       - /usr/bin/wg:/usr/bin/wg
@@ -34,13 +34,14 @@ services:
       DISABLE_REMOTE_IP_CHECK: "off" # If turned "on", Server will not set Host based on remote IP check. This is already overridden if SERVER_HOST is set. Turned "off" by default.
       GRPC_SSL: "off" # Tells clients to use SSL to connect to GRPC. Switch to on to turn on.
       COREDNS_ADDR: "" # Address of the CoreDNS server. Defaults to SERVER_HOST
+      DISPLAY_KEYS: "on" # Show keys permanently in UI (until deleted) as opposed to 1-time display.
       SERVER_API_CONN_STRING: "" # Changes the api connection string. IP:PORT format. By default is empty and uses SERVER_HOST:API_PORT
       SERVER_GRPC_CONN_STRING: "" # Changes the grpc connection string. IP:PORT format. By default is empty and uses SERVER_HOST:GRPC_PORT
   netmaker-ui: # The Netmaker UI Component
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.7
+    image: gravitl/netmaker-ui:v0.9.0
     links:
       - "netmaker:api"
     ports:

+ 3 - 2
compose/docker-compose.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.8.5
+    image: gravitl/netmaker:v0.9.0
     volumes:
       - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
       - /run/systemd/system:/run/systemd/system
@@ -34,12 +34,13 @@ services:
       SERVER_GRPC_WIREGUARD: "off"
       CORS_ALLOWED_ORIGIN: "*"
       DATABASE: "sqlite"
+      DISPLAY_KEYS: "on"
       NODE_ID: "netmaker-server-1"
   netmaker-ui:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.8.5
+    image: gravitl/netmaker-ui:v0.9.0
     links:
       - "netmaker:api"
     ports:

+ 2 - 0
config/config.go

@@ -44,6 +44,7 @@ type ServerConfig struct {
 	GRPCPort              string `yaml:"grpcport"`
 	GRPCSecure            string `yaml:"grpcsecure"`
 	MasterKey             string `yaml:"masterkey"`
+	DNSKey                string `yaml:"dnskey"`
 	AllowedOrigin         string `yaml:"allowedorigin"`
 	NodeID                string `yaml:"nodeid"`
 	RestBackend           string `yaml:"restbackend"`
@@ -66,6 +67,7 @@ type ServerConfig struct {
 	ClientID              string `yaml:"clientid"`
 	ClientSecret          string `yaml:"clientsecret"`
 	FrontendURL           string `yaml:"frontendurl"`
+	DisplayKeys           string `yaml:"displaykeys"`
 }
 
 // Generic SQL Config

+ 48 - 8
controllers/dnsHttpController.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"encoding/json"
 	"net/http"
+	"strings"
 
 	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
@@ -14,14 +15,14 @@ import (
 
 func dnsHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/dns", securityCheck(true, http.HandlerFunc(getAllDNS))).Methods("GET")
-	r.HandleFunc("/api/dns/adm/{network}/nodes", securityCheck(false, http.HandlerFunc(getNodeDNS))).Methods("GET")
-	r.HandleFunc("/api/dns/adm/{network}/custom", securityCheck(false, http.HandlerFunc(getCustomDNS))).Methods("GET")
-	r.HandleFunc("/api/dns/adm/{network}", securityCheck(false, http.HandlerFunc(getDNS))).Methods("GET")
-	r.HandleFunc("/api/dns/{network}", securityCheck(false, http.HandlerFunc(createDNS))).Methods("POST")
-	r.HandleFunc("/api/dns/adm/pushdns", securityCheck(false, http.HandlerFunc(pushDNS))).Methods("POST")
-	r.HandleFunc("/api/dns/{network}/{domain}", securityCheck(false, http.HandlerFunc(deleteDNS))).Methods("DELETE")
-	r.HandleFunc("/api/dns/{network}/{domain}", securityCheck(false, http.HandlerFunc(updateDNS))).Methods("PUT")
+	r.HandleFunc("/api/dns", securityCheckDNS(true, true, http.HandlerFunc(getAllDNS))).Methods("GET")
+	r.HandleFunc("/api/dns/adm/{network}/nodes", securityCheckDNS(false, true, http.HandlerFunc(getNodeDNS))).Methods("GET")
+	r.HandleFunc("/api/dns/adm/{network}/custom", securityCheckDNS(false, true, http.HandlerFunc(getCustomDNS))).Methods("GET")
+	r.HandleFunc("/api/dns/adm/{network}", securityCheckDNS(false, true, http.HandlerFunc(getDNS))).Methods("GET")
+	r.HandleFunc("/api/dns/{network}", securityCheckDNS(false, false, http.HandlerFunc(createDNS))).Methods("POST")
+	r.HandleFunc("/api/dns/adm/pushdns", securityCheckDNS(false, false, http.HandlerFunc(pushDNS))).Methods("POST")
+	r.HandleFunc("/api/dns/{network}/{domain}", securityCheckDNS(false, false, http.HandlerFunc(deleteDNS))).Methods("DELETE")
+	r.HandleFunc("/api/dns/{network}/{domain}", securityCheckDNS(false, false, http.HandlerFunc(updateDNS))).Methods("PUT")
 }
 
 //Gets all nodes associated with network, including pending nodes
@@ -408,3 +409,42 @@ func ValidateDNSUpdate(change models.DNSEntry, entry models.DNSEntry) error {
 	}
 	return err
 }
+
+//Security check DNS is middleware for every DNS function and just checks to make sure that its the master or dns token calling
+//Only admin should have access to all these network-level actions
+//DNS token should have access to only read functions
+func securityCheckDNS(reqAdmin bool, allowDNSToken bool, next http.Handler) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var errorResponse = models.ErrorResponse{
+			Code: http.StatusUnauthorized, Message: "W1R3: It's not you it's me.",
+		}
+
+		var params = mux.Vars(r)
+		bearerToken := r.Header.Get("Authorization")
+		if allowDNSToken && authenticateDNSToken(bearerToken) {
+			r.Header.Set("user", "nameserver")
+			networks, _ := json.Marshal([]string{ALL_NETWORK_ACCESS})
+			r.Header.Set("networks", string(networks))
+			next.ServeHTTP(w, r)
+		} else {
+			err, networks, username := SecurityCheck(reqAdmin, params["networkname"], bearerToken)
+			if err != nil {
+				if strings.Contains(err.Error(), "does not exist") {
+					errorResponse.Code = http.StatusNotFound
+				}
+				errorResponse.Message = err.Error()
+				returnErrorResponse(w, r, errorResponse)
+				return
+			}
+			networksJson, err := json.Marshal(&networks)
+			if err != nil {
+				errorResponse.Message = err.Error()
+				returnErrorResponse(w, r, errorResponse)
+				return
+			}
+			r.Header.Set("user", username)
+			r.Header.Set("networks", string(networksJson))
+			next.ServeHTTP(w, r)
+		}
+	}
+}

+ 21 - 3
controllers/networkHttpController.go

@@ -114,10 +114,16 @@ func SecurityCheck(reqAdmin bool, netname string, token string) (error, []string
 
 //Consider a more secure way of setting master key
 func authenticateMaster(tokenString string) bool {
-	if tokenString == servercfg.GetMasterKey() {
-		return true
+	return tokenString == servercfg.GetMasterKey()
+}
+
+//Consider a more secure way of setting master key
+func authenticateDNSToken(tokenString string) bool {
+	tokens := strings.Split(tokenString, " ")
+	if len(tokens) < 2 {
+		return false
 	}
-	return false
+	return tokens[1] == servercfg.GetDNSKey()
 }
 
 //simple get all networks function
@@ -146,6 +152,12 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 	}
+	if !servercfg.IsDisplayKeys() {
+		for i, net := range allnetworks {
+			net.AccessKeys = logic.RemoveKeySensitiveInfo(net.AccessKeys)
+			allnetworks[i] = net
+		}
+	}
 	functions.PrintUserLog(r.Header.Get("user"), "fetched networks.", 2)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(allnetworks)
@@ -183,6 +195,9 @@ func getNetwork(w http.ResponseWriter, r *http.Request) {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	if !servercfg.IsDisplayKeys() {
+		network.AccessKeys = logic.RemoveKeySensitiveInfo(network.AccessKeys)
+	}
 	functions.PrintUserLog(r.Header.Get("user"), "fetched network "+netname, 2)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
@@ -572,6 +587,9 @@ func getAccessKeys(w http.ResponseWriter, r *http.Request) {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	if !servercfg.IsDisplayKeys() {
+		keys = logic.RemoveKeySensitiveInfo(keys)
+	}
 	functions.PrintUserLog(r.Header.Get("user"), "fetched access keys on network "+network, 2)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(keys)

+ 19 - 4
controllers/nodeGrpcController.go

@@ -29,10 +29,14 @@ func (s *NodeServiceServer) ReadNode(ctx context.Context, req *nodepb.Object) (*
 	if err != nil {
 		return nil, err
 	}
+	node.NetworkSettings, err = logic.GetNetworkSettings(node.Network)
+	if err != nil {
+		return nil, err
+	}
 	node.SetLastCheckIn()
 	// Cast to ReadNodeRes type
-	nodeData, err := json.Marshal(&node)
-	if err != nil {
+	nodeData, errN := json.Marshal(&node)
+	if errN != nil {
 		return nil, err
 	}
 	logic.UpdateNode(&node, &node)
@@ -75,7 +79,14 @@ func (s *NodeServiceServer) CreateNode(ctx context.Context, req *nodepb.Object)
 	if err != nil {
 		return nil, err
 	}
-	nodeData, err := json.Marshal(&node)
+	node.NetworkSettings, err = logic.GetNetworkSettings(node.Network)
+	if err != nil {
+		return nil, err
+	}
+	nodeData, errN := json.Marshal(&node)
+	if errN != nil {
+		return nil, err
+	}
 	// return the node in a CreateNodeRes type
 	response := &nodepb.Object{
 		Data: string(nodeData),
@@ -107,10 +118,14 @@ func (s *NodeServiceServer) UpdateNode(ctx context.Context, req *nodepb.Object)
 	if err != nil {
 		return nil, err
 	}
-	nodeData, err := json.Marshal(&newnode)
+	newnode.NetworkSettings, err = logic.GetNetworkSettings(node.Network)
 	if err != nil {
 		return nil, err
 	}
+	nodeData, errN := json.Marshal(&newnode)
+	if errN != nil {
+		return nil, err
+	}
 	return &nodepb.Object{
 		Data: string(nodeData),
 		Type: nodepb.NODE_TYPE,

+ 1 - 1
controllers/relay.go

@@ -37,7 +37,7 @@ func createRelay(w http.ResponseWriter, r *http.Request) {
 // CreateRelay - creates a relay
 func CreateRelay(relay models.RelayRequest) (models.Node, error) {
 	node, err := logic.GetNodeByMacAddress(relay.NetID, relay.NodeID)
-	if node.OS == "windows" || node.OS == "macos" { // add in darwin later
+	if node.OS == "macos" { // add in darwin later
 		return models.Node{}, errors.New(node.OS + " is unsupported for relay")
 	}
 	if err != nil {

+ 26 - 13
docker/Dockerfile-netclient-full

@@ -1,26 +1,39 @@
-#first stage - builder
+FROM gravitl/builder:latest as builder
+# add glib support daemon manager
+WORKDIR /app
 
-FROM golang:latest as builder
+COPY . .
 
-COPY . /app
+ENV GO111MODULE=auto
 
-WORKDIR /app/netclient
+RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 /usr/local/go/bin/go build -ldflags="-w -s" -o netclient-app netclient/main.go
 
-ENV GO111MODULE=auto
+WORKDIR /root/
 
-RUN CGO_ENABLED=0 GOOS=linux go build -o netclient main.go
+RUN apk add --update git build-base libmnl-dev iptables
 
-#second stage
+RUN git clone https://git.zx2c4.com/wireguard-go && \
+    cd wireguard-go && \
+    make && \
+    make install
 
-FROM debian:latest
+ENV WITH_WGQUICK=yes
+RUN git clone https://git.zx2c4.com/wireguard-tools && \
+    cd wireguard-tools && \
+    cd src && \
+    make && \
+    make install
 
-RUN apt-get update && apt-get -y install systemd procps
+FROM alpine:3.13.6
 
 WORKDIR /root/
 
-COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
-
-COPY --from=builder /app/netclient/netclient .
+RUN apk add --no-cache --update bash libmnl iptables openresolv iproute2
+COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
+COPY --from=builder /app/netclient-app ./netclient
+COPY --from=builder /app/scripts/netclient.sh .
+RUN chmod 0755 netclient && chmod 0755 netclient.sh
 
-CMD ["./netclient"]
+ENV WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard-go
 
+ENTRYPOINT ["/bin/sh", "./netclient.sh"]

+ 1 - 1
docker/Dockerfile-userspace

@@ -21,6 +21,6 @@ FROM gravitl/netmaker:${NM_VERSION}
 
 RUN apk add --no-cache --update bash libmnl iptables openresolv iproute2
 COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
-COPY scripts/userspace-entrypoint.sh ./entrypoint.sh
+COPY scripts/netclient.sh ./entrypoint.sh
 
 ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]

BIN
docs/_build/doctrees/about.doctree


BIN
docs/_build/doctrees/api.doctree


BIN
docs/_build/doctrees/architecture.doctree


BIN
docs/_build/doctrees/client-installation.doctree


BIN
docs/_build/doctrees/conduct.doctree


BIN
docs/_build/doctrees/environment.pickle


BIN
docs/_build/doctrees/external-clients.doctree


BIN
docs/_build/doctrees/getting-started.doctree


BIN
docs/_build/doctrees/index.doctree


BIN
docs/_build/doctrees/install.doctree


BIN
docs/_build/doctrees/license.doctree


BIN
docs/_build/doctrees/oauth.doctree


BIN
docs/_build/doctrees/quick-start-nginx.doctree


BIN
docs/_build/doctrees/quick-start.doctree


BIN
docs/_build/doctrees/server-installation.doctree


BIN
docs/_build/doctrees/support.doctree


BIN
docs/_build/doctrees/troubleshoot.doctree


BIN
docs/_build/doctrees/usage.doctree


+ 4 - 0
docs/_build/html/.buildinfo

@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 33ce1e4f23d51ede6f4c252bd38dd615
+tags: 645f666f9bcd5a90fca523b33c5a78b7

BIN
docs/_build/html/_images/access-key.png


BIN
docs/_build/html/_images/create-net.png


BIN
docs/_build/html/_images/exclient1.png


BIN
docs/_build/html/_images/exclient2.png


BIN
docs/_build/html/_images/exclient3.png


BIN
docs/_build/html/_images/exclient4.png


BIN
docs/_build/html/_images/extclient5.png


BIN
docs/_build/html/_images/mesh-diagram.png


BIN
docs/_build/html/_images/mesh.png


BIN
docs/_build/html/_images/nc-install-output.png


BIN
docs/_build/html/_images/netmaker-node.png


BIN
docs/_build/html/_images/netmaker.png


BIN
docs/_build/html/_images/nm-diagram-2.jpg


BIN
docs/_build/html/_images/nm-node-success.png


BIN
docs/_build/html/_images/node-details.png


BIN
docs/_build/html/_images/nodes.png


BIN
docs/_build/html/_images/oauth1.png


BIN
docs/_build/html/_images/oauth2.png


BIN
docs/_build/html/_images/oauth3.png


BIN
docs/_build/html/_images/ping-node.png


+ 46 - 0
docs/_build/html/_sources/about.rst.txt

@@ -0,0 +1,46 @@
+===============
+About
+===============
+
+What is Netmaker?
+==================
+
+Netmaker is a tool for creating and managing virtual overlay networks. If you have at least two machines with internet access which you need to connect with a secure tunnel, Netmaker is for you. If you have thousands of servers spread across multiple locations, data centers, or clouds, Netmaker is also for you. Netmaker connects machines securely, wherever they are.
+
+.. image:: images/mesh-diagram.png
+   :width: 50%
+   :alt: WireGuard Mesh
+   :align: center
+
+Netmaker takes those machines and creates a flat network so that they can all talk to each other easily and securely. 
+If you're familiar with AWS, it's like a VPC but made up of arbitrary computers. From the machine's perspective, all these other machines are in the same neighborhood, even if they're spread all over the world.
+
+Netmaker has many similarities to Tailscale, ZeroTier, and Nebula. What makes Netmaker different is its speed and flexibility. Netmaker is faster because it uses kernel WireGuard. It is more dynamic because the server and agents are fully configurable, which lets you handle all sorts of different use cases.
+
+How Does Netmaker Work?
+=======================
+
+Netmaker relies on WireGuard to create tunnels between machines. At its core, Netmaker is managing WireGuard across machines to create sensible networks. Technically, Netmaker is two things:
+
+- the admin server, called Netmaker
+- the agent, called Netclient
+
+As the network manager, you interact with the server to create and manage networks and devices. The server holds configurations for these networks and devices, which are retrieved by the netclients (agent). 
+
+The netclient is installed on any machine you would like to add to a given network, whether that machine is a VM, Server, or IoT device. The netclient reaches out to the server, and the server tells it how it should configure the network. By doing this across many machines simultaneously, we create a dynamic, fully configurable virtual networks.
+
+The Netmaker server does not typically route traffic. Otherwise, this would be a hub-and-spoke model, which is very slow. Instead, Netmaker just tells the machines on the network how they can reach each other directly. This is called a *full mesh* network and is much faster. Even if the server goes down, as long as none of the existing machines change substantially, your network will still run just fine.
+
+Use Cases for Netmaker
+=============================
+
+There are many use cases for Netmaker. In fact, you could probably be using it right now. This list is not all-encompassing, but provides a sample of how you might want to use Netmaker. Guided setup for many of these use cases can be found in the :doc:`Using Netmaker <./usage>` documentation. 
+
+ 0. Automate creation of a WireGuard mesh network
+ 1. Create a flat, secure network between cloud environments and data centers
+ 2. Provide secure access to IoT devices, remote servers, and client sites.
+ 3. Secure a home or office network
+ 4. Add a layer of encryption to an existing network 
+ 5. Secure site-to-site connections
+ 6. Manage cryptocurrency proof-of-stake machines 
+ 7. Create a dynamic and secure Kubernetes underlay network

+ 184 - 0
docs/_build/html/_sources/api.rst.txt

@@ -0,0 +1,184 @@
+=============================================
+API Reference
+=============================================
+
+API Usage
+==========================
+
+Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.
+
+
+Authentication
+==============
+API calls must be authenticated via a header of  the format  `-H "Authorization: Bearer <YOUR_SECRET_KEY>"` There are two methods to obtain YOUR_SECRET_KEY:
+1. Using the masterkey. By default, this value is "secret key," but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [general usage](./USAGE.md) documentation for more details.
+2. Using a JWT recieved for a node. This  can be retrieved by calling the `/api/nodes/<network>/authenticate` endpoint, as documented below.
+
+
+Format of Calls for Curl
+========================
+Requests take the format of `curl -H "Authorization: Bearer <YOUR_SECRET_KEY>" -H 'Content-Type: application/json' localhost:8081/api/path/to/endpoint`
+
+
+API Documentation
+=================
+
+Networks API
+------------
+
+**Get All Networks:** `/api/networks`, `GET` 
+  
+**Create Network:** `/api/network`, `POST` 
+  
+**Get Network:** `/api/networks/{network id}`, `GET`  
+  
+**Update Network:** `/api/networks/{network id}`, `PUT`  
+  
+**Delete Network:** `/api/networks/{network id}`, `DELETE`  
+  
+**Cycle PublicKeys on all Nodes:** `/api/networks/{network id}/keyupdate`, `POST`  
+  
+  
+Networks API Call Examples
+--------------------------  
+  
+**Get All Networks:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks | jq`
+
+**Create Network:** `curl -d '{"addressrange":"10.70.0.0/16","netid":"skynet"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks`
+
+**Get Network:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet | jq`
+
+**Update Network:** `curl -X PUT -d '{"displayname":"my-house"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks/skynet`
+
+**Delete Network:** `curl -X DELETE -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet`
+
+**Cycle PublicKeys on all Nodes:** `curl -X POST -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keyupdate`
+
+Access Keys API
+---------------
+
+**Get All Keys:** `/api/networks/{network id}/keys`, `GET` 
+  
+**Create Key:** `/api/networks/{network id}/keys`, `GET` 
+  
+**Delete Key:** `/api/networks/{network id}/keys/{keyname}`, `DELETE` 
+  
+  
+Access Keys API Call Examples
+-----------------------------
+   
+**Get All Keys:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keys | jq`
+  
+**Create Key:** `curl -d '{"uses":10,"name":"mykey"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks/skynet/keys`
+  
+**Delete Key:** `curl -X DELETE -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keys/mykey`
+  
+    
+Nodes API
+---------
+  
+**Get All Nodes:** `/api/nodes`, `GET` 
+  
+**Get Network Nodes:** `/api/nodes/{network id}`, `GET` 
+  
+**Create Node:** `/api/nodes/{network id}`, `POST`  
+  
+**Get Node:** `/api/nodes/{network id}/{macaddress}`, `GET`  
+  
+**Update Node:** `/api/nodes/{network id}/{macaddress}`, `PUT`  
+  
+**Delete Node:** `/api/nodes/{network id}/{macaddress}`, `DELETE`  
+  
+**Check In Node:** `/api/nodes/{network id}/{macaddress}/checkin`, `POST`  
+  
+**Create a Gateway:** `/api/nodes/{network id}/{macaddress}/creategateway`, `POST`  
+  
+**Delete a Gateway:** `/api/nodes/{network id}/{macaddress}/deletegateway`, `DELETE`  
+  
+**Uncordon (Approve) a Pending Node:** `/api/nodes/{network id}/{macaddress}/uncordon`, `POST`  
+  
+**Get Last Modified Date (Last Modified Node in Network):** `/api/nodes/adm/{network id}/lastmodified`, `GET`  
+  
+**Authenticate:** `/api/nodes/adm/{network id}/authenticate`, `POST`  
+  
+  
+Nodes API Call Examples
+----------------------- 
+  
+**Get All Nodes:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes | jq`
+  
+**Get Network Nodes:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes/skynet | jq`
+    
+**Create Node:** `curl  -d  '{ "endpoint": 100.200.100.200, "publickey": aorijqalrik3ajflaqrdajhkr,"macaddress": "8c:90:b5:06:f1:d9","password": "reallysecret","localaddress": "172.16.16.1","accesskey": "aA3bVG0rnItIRXDx","listenport": 6400}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet`
+    
+**Get Node:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes/skynet/{macaddress} | jq`  
+  
+**Update Node:** `curl -X PUT -d '{"name":"laptop1"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9`
+  
+**Delete Node:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/skynet/nodes/8c:90:b5:06:f1:d9`
+  
+**Create a Gateway:** `curl  -d  '{ "rangestring": "172.31.0.0/16", "interface": "eth0"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/creategateway`
+  
+**Delete a Gateway:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/deletegateway`
+  
+**Approve a Pending Node:** `curl -X POST -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/approve`
+  
+**Get Last Modified Date (Last Modified Node in Network):** `curl -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/adm/skynet/lastmodified`
+
+**Authenticate:** `curl -d  '{"macaddress": "8c:90:b5:06:f1:d9", "password": "YOUR_PASSWORD"}' -H 'Content-Type: application/json' localhost:8081/api/nodes/adm/skynet/authenticate`
+  
+
+Users API
+-----------------------
+  
+**Note:** Only able to create Admin user at this time. The "user" is only used by the `user interface <https://github.com/gravitl/netmaker-ui>`_ to authenticate the  single  admin user.
+
+**Get User:** `/api/users/{username}`, `GET`  
+  
+**Update User:** `/api/users/{username}`, `PUT`  
+  
+**Delete User:** `/api/users/{username}`, `DELETE`  
+  
+**Check for Admin User:** `/api/users/adm/hasadmin`, `GET` 
+  
+**Create Admin User:** `/api/users/adm/createadmin`, `POST` 
+  
+**Authenticate:** `/api/users/adm/authenticate`, `POST` 
+  
+  
+Users API Calls Examples
+------------------------
+  
+**Get User:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/users/{username} | jq`
+
+**Update User:** `curl -X PUT -d '{"password":"noonewillguessthis"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/{username}`
+  
+**Delete User:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/{username}`
+  
+**Check for Admin User:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/users/adm/hasadmin`
+  
+**Create Admin User:** `curl -d '{ "username": "smartguy", "password": "YOUR_PASS"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/adm/createadmin`
+   
+**Authenticate:** `curl -d  '{"username": "smartguy", "password": "YOUR_PASS"}' -H 'Content-Type: application/json' localhost:8081/api/nodes/adm/skynet/authenticate`
+  
+
+Server Management API
+---------------------
+
+The Server Mgmt. API allows you to add and remove the server from networks.
+
+**Add to Network:** `/api/server/addnetwork/{network id}`, `POST`  
+  
+**Remove from Network:** `/api/server/removenetwork/{network id}`, `DELETE`  
+
+**Add to Network:**  `curl -X POST -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/server/addnetwork/{network id}`
+
+**Remove from Network:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/server/removenetwork/{network id}`
+
+
+File Server API
+---------------
+  
+**Get File:** `/meshclient/files/{filename}`, `GET`
+  
+**Example:**  `curl localhost:8081/meshclient/files/meshclient`

+ 196 - 0
docs/_build/html/_sources/architecture.rst.txt

@@ -0,0 +1,196 @@
+===============
+Architecture
+===============
+
+.. image:: images/nm-diagram-2.jpg
+   :width: 45%
+   :alt: Netmaker Architecture Diagram
+   :align: center
+    
+
+*Pictured Above: A diagram of Netmaker's Architecture.*
+
+
+Core Concepts
+==============
+
+Familiarity with several core concepts will help when you encounter them later on in the documentation.
+
+WireGuard
+----------
+
+WireGuard is a relatively new but very important technology which was recently added to the Linux kernel. WireGuard creates very fast but simple encrypted tunnels between devices. From the `WireGuard <https://www.wireguard.com/>`_ website, "it might be regarded as the most secure, easiest to use, and simplest VPN solution in the industry."
+
+Previous solutions like OpenVPN and IPSec are considerably more heavy and complex, while being less performant. All existing VPN tunnelling solutions will cause a significant increase in your network latency. WireGuard is the first to achieve near over-the-line network speeds, meaning you see no signigifant performance impact.  With the release of WireGuard, there is little reason to use any other existing tunnel encryption technology.
+
+Mesh Network
+-------------
+
+When we refer to a mesh network in these documents we are typically referring to a "full mesh."
+
+.. image:: images/mesh.png
+   :width: 33%
+   :alt: Full Mesh Network Diagram
+   :align: center
+
+
+A full `mesh network <https://www.bbc.co.uk/bitesize/guides/zr3yb82/revision/2>`_ exists where each machine is able to directly talk to every other machine on the network. For example, on your home network, behind your router, all the computers are likely given private addresses and can reach each other directly.
+
+This is in contrast to a hub-and-spoke network, where each machine must first pass its traffic through a relay server before it can reach other machines.
+
+In certain situations you may either want or need a *partial mesh* network, where only some devices can reach each other directly, and other devices must route their traffic through a relay/gateway. Netmaker can use this model in some use cases where it makes sense. In the diagram at the top of this page, the setup is a partial mesh, because the servers (nodes A-D) are meshed, but then external clients come in via a gateway, and are not meshed.
+
+Mesh networks are generally faster than other topologies, but are also more complicated to set up. WireGuard on its own gives you the means to create encrypted tunnels between devices, but it does not provide a method for setting up a full network. This is where Netmaker comes in.
+
+Netmaker
+---------
+
+Netmaker is a platform built off of WireGuard which enables users to create mesh networks between their devices. Netmaker can create both full and partial mesh networks depending on the use case.
+
+When we refer to Netmaker in aggregate, we are typically referring to Netmaker and the netclient, as well as other supporting services such as CoreDNS, rqlite, and UI webserver.
+
+From an end user perspective, they typically interact with the Netmaker UI, or even just run the install script for the netclient on their devices. The other components run in the background invisibly. 
+
+Netmaker does a lot of work to set configurations for you, so that you don't have to. This includes things like WireGuard ports, endpoints, public IPs, keys, and peers. Netmaker works to abstract away as much of the network management as possible, so that you can just click to create a network, and click to add a machine to a network. That said, every machine (node) is different, and may require special configuration. That is why, while Netmaker sets practical default settings, everything within Netmaker is fully configurable.
+
+Node
+------
+
+A machine in a Netmaker network, which is managed by the Netclient, is referred to as a Node, as you will see in the UI. A Node can be a VM, a bare metal server, a desktop computer, an IoT device, or any other number of internet-connected machines on which the netclient is installed. A node is simply an endpoint in the network, which can send traffic to all the other nodes, and recieve traffic from all of the other nodes.
+
+SystemD
+-------
+
+SystemD is a system service manager for a wide array of Linux operating systems. Not all Linux distributions have adopted systemd, but, for better or worse, it has become a fairly common standard in the Linux world. That said, any non-Linux operating system will not have systemd, and many Linux/Unix distributionshave alternative system service managers.
+
+Netmaker's netclient, the agent which controls networking on all nodes, can be run as a CLI or as a system daemon. On Linux, it runs as a daemon by default, and this requires systemd. As Netmaker evolves, systemd will become just one of the possible service management options, allowing the netclient to be run on a wider array of devices. However, for the time being, the netclient should be run "unmanaged" (netclient join -daemon=off) on systems that do not run systemd, and some other method can be used like a cron job or custom script.
+
+As of 0.8, Mac and Windows are supported. On these operating systems, netclient launches the daemon using LaunchD and Windows Service, respectively, as opposed to SystemD.
+
+Components
+===========
+
+Netmaker consists of several core components, which are explained in high-level technical detail below.
+
+Netmaker Server
+------------------
+
+The Netmaker server is, at its core, a golang binary. Source code can be found `on GitHub <https://github.com/gravitl/netmaker>`_. The binary, by itself can be compiled for most systems. If you need to run the Netmaker server on a particular system, it likely can be made to work. In typical deployments, it is run as a Docker container. It can also be run as a systemd service as outlined in the non-docker install guide.
+
+The Netmaker server acts as an API to the front end, and as a GRPC server to the machines in the network. GRPC is much faster and more efficient than standard API calls, which increases the speed of transactions. For this reason, the Netmaker server exposes two ports: The default for the API is 8081, and the default for GRPC is 50051. Either the API or the GRPC server can be disabled on any given Netmaker instance, allowing you to deploy two different servers for managing the API (which is largely for the admin's use) and GRPC (which is largely for the nodes' use).
+
+Most server settings are configurable via a config file, or by environment variables (which take precedence). If the server finds neither of these, it sets sensible defaults, including things like the server's reachable IP, ports, and which "modes" to run in.
+
+These modes include client mode and dns mode. Either of these can be disabled but are enabled by default. Client mode allows you to treat the Netmaker host machine (operating system) as a network Node, installing the netclient and controlling the host network. DNS mode has the server write config settings for CoreDNS, a separate component and nameserver, which picks up the config settings to manage node DNS.
+
+The Netmaker server interacts with either sqlite (default), postgres, or rqlite, a distributed version of sqlite, as its database. This DB holds information about nodes, networks, users, and other important data. This data is configuration data. For the most part, Netmaker serves configuration data to Nodes, telling them how they should configure themselves. The Netclient is the agent that actually does that configuration.
+
+
+Netclient
+----------------
+
+The netclient is, at its core, a golang binary. Source code can be found in the netclient folder of the Netmaker `GitHub Repository <https://github.com/gravitl/netmaker/tree/master/netclient>`_. The binary, by itself, can be compiled for most systems. However, this binary is designed to manage a certain number of Operating Systems. As of version 0.8, the netclient can be run as a system daemon on linux distributions with systemd, or as an "unmanaged" client on distributions without systemd. The netclient for Windows and Mac will run as a Windows Service or LaunchDaemon, respectively.
+
+The netclient is installed via a simple bash script, which pulls the latest binary and runs 'register' and 'join' commands.
+
+The 'register' command adds a WireGuard tunnel directly to the netmaker server, for all subsequent communication.
+
+The 'join' command attempts to add the machine to the Netmaker network using sensible defaults, which can be overridden with a config file or environment variables. Assuming the netclient has a valid key (or the network allows manual node signup), it will be registered into the Netmaker network, and will be returned necessary configuration details for how to set up its local network. 
+
+The netclient then sets up the system daemon (if running in daemon mode), and configures WireGuard. At this point it should be part of the network.
+
+If running in daemon mode, on a periodic basis (systemd timer), the netclient performs a "check in." It will authenticate with the server, and check to see if anything has changed in the network. It will also post changes about its own local configuration if there. If there has been a change, the server will return new configurations and the netclient will reconfigure the network. If not running in daemon mode, it is up to the operator to perform check ins (netclient checkin -n < network name >).
+
+The check in process is what allows Netmaker to create dynamic mesh networks. As nodes are added to, removed from, and modified on the network, other nodes are notified, and make appropriate changes.
+
+
+Database (sqlite, rqlite, postgres)
+-------------------------------------
+
+As of v0.8, Netmaker uses sqlite by default as a database. It can also use PostgreSQL, or rqlite, a distributed (RAFT consensus) databaseand. Netmaker interacts with this database to store and retrieve information about nodes, networks, and users. 
+
+Additional database support (besides sqlite and rqlite) is very easy to implement for special use cases. Netmaker uses simple key value lookups to run the networks, and the database was designed to be extensible, so support for key-value stores and other SQL-based databases can be achieved by changing a single file.
+
+Netmaker UI
+---------------
+
+The Netmaker UI is a ReactJS-based static website which can be run on top of standard webservers such as Apache and Nginx. Source code can be found `here <https://github.com/gravitl/netmaker-ui>`_. In a typical configuration, the Netmaker UI is run on Nginx as a Docker container.
+
+Netmaker can be used in its entirety without the UI, but the UI makes things a lot easier for most users. It has a sensible flow and layout for managing Networks, Nodes, Access Keys, and DNS.
+
+
+CoreDNS
+--------
+
+Netmaker allows users to provide and manage Private DNS for their nodes. This requires a nameserver, and CoreDNS is the chosen nameserver. CoreDNS is lightweight and extensible. CoreDNS loads dns settings from a simple file, managed by Netmaker, and serves out DNS info for managed nodes. DNS can be tricky, and DNS management is currently only supported on a small set of devices, specifically those running systemd-resolved. However, the Netmaker CoreDNS instance can be added manually as a nameserver to other devices. DNS mode can also be turned off.
+
+External Client
+----------------
+
+The external client is simply a manually configured WireGuard connection to your network, which Netmaker helps to manage.
+
+Most machines can run WireGuard. It is fairly simple to set up a WireGuard connection to a single endpoint. It is setting up mesh networks and other topologies like site-to-site which becomes complicated. 
+
+Mac, Windows, and Linux are handled natively by the Netclient.
+
+Netmaker can issue "external clients" to handle any devices which are not currently compatible with the netclient, including iPhone, Android, and some Unix distributions. Over time, this list will be eliminated and there may not even be a need for the external client.
+
+External clients hook into a Netmaker network via an "Ingress Gateway," which is configured for a given node and allows traffic to flow into the network.
+
+Technical Process
+====================
+
+Below is a high level, step-by-step overview of the flow of communications within Netmaker (assuming Netmaker has already been installed):
+
+1. Admin creates a new network with a subnet, for instance 10.10.10.0/24
+2. Admin creates an access key for signing up new nodes
+3. Both of the above requests are routed to the server via an API call from the front end
+4. Admin runs the netclient install script on any given node (machine).
+5. Netclient decodes key, which contains the GRPC server location and port
+6. Netclient uses information to register and set up WireGuard tunnel to GRPC server
+7. Netclient retrieves/sets local information, including open ports for WireGuard, public IP, and generating key pairs for peers
+8. Netclient reaches out to GRPC server with this information, authenticating via access key.
+9. Netmaker server verifies information and creates the node, setting default values for any missing information. 
+10. Timestamp is set for the network (see #16). 
+11. Netmaker returns settings as response to netclient. Some settings may be added or modified based on the network.
+12. Netclient recieves response. If successful, it takes any additional info returned from Netmaker and configures the local system/WireGuard
+13. Netclient sends another request to Netmaker's GRPC server, this time to retrieve the peers list (all other clients in the network).
+14. Netmaker sends back peers list, including current known configurations of all nodes in network.
+15. Netclient configures WireGuard with this information. At this point, the node is fully configured as a part of the network and should be able to reach the other nodes via private address.
+16. Netclient begins daemon (system timer) to run check in's with the server. It awaits changes, reporting local changes, and retrieving changes from any other nodes in the network.
+17. Other netclients on the network, upon checking in with the Netmaker server, will see that the timestamp has updated, and they will retrieve a new peers list, completing the update cycle.
+
+
+Compatible Systems for Netclient
+==================================
+
+To manage a node manually, the Netclient can be compiled and run for most linux distibutions, with a prerequisite of WireGuard with kernel headers. If the netclient from the release pages does not run natively on your system, you may need to compile the netclient binary directly on the machine from the source code. This may be true for some installations of SUSE, Fedora, and some Debian-based systems. However, if the dependencies are installed on the machine, the netclient should run correctly after being compiled.
+
+Simply clone the repo, cd to netmaker/netclient and run "go build" (Golang must be installed).
+
+The following systems should be operable natively with Netclient in daemon mode:
+        - Windows
+        - Mac
+        - FreeBSD
+        - OpenWRT
+        - Fedora
+        - Ubuntu
+        - Debian
+        - Mint
+        - SUSE
+        - RHEL
+        - Raspian.
+        - Arch
+        - CentOS
+        - CoreOS
+
+To manage DNS (optional), the node must have systemd-resolved. Systems that have this enabled include:
+        - Arch
+        - Debian
+        - Ubuntu
+        - SUSE
+
+Limitations
+=============
+
+Install limitations mostly include platform-specific dependencies. A failed netclient install should display information about which command is failing, or which libraries are missing. This can often be solved via machine upgrade, installing missing dependencies, or setting kernel headers on the machine for WireGuard (e.x.: `Installing Kernel Headers on Debian <https://stackoverflow.com/questions/62356581/wireguard-vpn-how-to-fix-operation-not-supported-if-it-worked-before>`_) 

+ 160 - 0
docs/_build/html/_sources/client-installation.rst.txt

@@ -0,0 +1,160 @@
+====================
+Client Installation
+====================
+
+This document tells you how to install the netclient on machines that will be a part of your Netmaker network, as well as non-compatible systems.
+
+These steps should be run after the Netmaker server has been created and a network has been designated within Netmaker.
+
+Introduction to Netclient
+===============================
+
+At its heart, the netclient is a simple CLI for managing access to various WireGuard-based networks. It manages WireGuard on the host system, so that you don't have to. Why is this necessary?
+
+If you are setting up a WireGuard-based virtual network, you must configure each machine with very specific settings, so that every machine can reach it, and it can reach every machine. Any changes to the settings of any one of these machines can break those connections. Any machine that is added, removed, or modified on the network requires reconfiguring every peer in the network. This can be very time consuming.
+
+The netmaker server holds configuration details about every machine in your network and how other machines should connect to it.
+
+The netclient agent connects to the server, pushing and pulling information when the network (or its local configuration) changes. 
+
+The netclient agent then configures WireGuard (and other network properties) locally, so that the network stays intact.
+
+Notes on Windows
+==================================
+
+If running the netclient on windows, you must download the netclient.exe binary and run it from Powershell as an Administrator.
+
+Windows will by default have firewall rules that prevent inbound connections. If you wish to allow inbound connections from particular peers, use the following command:
+
+``netsh advfirewall firewall add rule name="Allow from <peer private addr>" dir=in action=allow protocol=ANY remoteip=<peer private addr>``
+
+If you want to allow all peers access, but do not want to configure firewall rules for all peers, you can configure access for one peer, and set it as a Relay Server.
+
+Modes and System Compatibility
+==================================
+
+**Note: If you would like to connect non-Linux/Unix machines to your network such as phones and Windows desktops, please see the documentation on External Clients**
+
+The netclient can be run in a few "modes". System compatibility depends on which modes you intend to use. These modes can be mixed and matched across a network, meaning all machines do not have to run with the same "mode."
+
+CLI
+------------
+
+In its simplest form, the netclient can be treated as just a simple, manual, CLI tool, which a user can call to configure the machine. The cli can be compiled from source code to run on most systems, and has already been compiled for x86 and ARM devices.
+
+As a CLI, the netclient should function on any Linux or Unix based system that has the wireguard utility (callable with **wg**) installed.
+
+Daemon
+----------
+
+The netclient is intended to be run as a system daemon. This allows it to automatically retrieve and send updates. To do this, the netclient can install itself as a systemd service, or launchd/windows service for Mac or Windows.
+
+If running the netclient on non-systemd linux, it is recommended to manually configure the netclient as a daemon using whatever method is acceptable on the chosen operating system.
+
+Private DNS Management
+-----------------------
+
+To manage private DNS, the netclient relies on systemd-resolved (resolvectl). Absent this, it cannot set private DNS for the machine.
+
+A user may choose to manually set a private DNS nameserver of <netmaker server>:53. However, beware, as netmaker sets split dns, and the system must be configured properly. Otherwise, this nameserver may break your local DNS.
+
+Prerequisites
+=============
+
+To obtain the netclient, go to the GitHub releases: https://github.com/gravitl/netmaker/releases
+
+**For netclient cli:** Linux/Unix with WireGuard installed (wg command available)
+
+**For netclient daemon:** Systemd Linux + WireGuard
+
+**For Private DNS management:** Resolvectl (systemd-resolved)
+
+Configuration
+===============
+
+The CLI has information about all commands and variables. This section shows the "help" output for these commands as well as some additional reference.
+
+CLI Reference
+--------------------
+``sudo netclient --help``
+
+.. literalinclude:: ./examplecode/netclient-help.txt
+  :language: YAML
+
+
+``sudo netclient join --help``
+
+.. literalinclude:: ./examplecode/netclient-join.txt
+  :language: YAML
+
+
+Config File Reference
+------------------------
+
+There is a config file for each node under /etc/netconfig-<network name>. You can change these values and then set "postchanges" to "true", or go to the CLI and run ``netclient push -n <network>``
+
+
+.. literalinclude:: ./examplecode/netconfig-example.yml
+  :language: YAML
+
+
+Installation
+======================
+
+
+To install netmaker, you need a server token for a particular network, unless you're joining a network that allows manual signup, in which case you can join without a token, but the server will quarantine the machine until the admin approves it.
+
+An admin creates a token in the ACCESS KEYS section of the UI. Upon creating a token, it generates 3 values:
+
+**Access Key:** The secret key to authenticate as a node in the network
+
+**Access Token:** The secret key plus information about how to access the server (addresses, ports), all decoded by the netclient to register with the server
+
+**Install Command:** A short script that will obtain the netclient binary, register with the server, and join the network, all in one
+
+For first time installations, you can run the Install Command. For additional networks, simply run ``netclient join -t <access token>``. The raw access key will not be needed unless there are special circumstances, mostly troubleshooting incorrect information in the token (you can instead manually specify the server location).
+
+
+Managing Netclient
+=====================
+
+Viewing Logs
+---------------
+
+**to view current networks**
+  ``netclient list``
+
+**to tail logs**
+  ``journalctl -u netclient@<net name> -f``
+
+**to view all logs**
+  ``journalctl -u netclient@<net name>``
+
+**to get most recent log run**
+  ``systemctl status netclient@<net name>``
+
+Making Updates
+----------------
+
+``vim /etc/netclient/netconfig-<network>``
+
+Change any of the variables in this file, and changes will be pushed to the server and processed locally on the next checkin.
+
+For instance, change the private address, endpoint, or name. See above example config file for details
+
+
+Adding/Removing Networks
+---------------------------
+
+``netclient join -t <token>``
+
+Set any of the above flags (netclient join --help) to override settings for joining the network. 
+If a key is provided (-k), then a token is unnecessary, but grpc, server, ports, and network must all be provided via flags.
+
+
+Uninstalling
+---------------
+
+``netclient uninstall``
+
+

+ 77 - 0
docs/_build/html/_sources/conduct.rst.txt

@@ -0,0 +1,77 @@
+===============
+Code of Conduct
+===============
+
+Our Pledge
+==========
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+Our Standards
+=============
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+Our Responsibilities
+====================
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+Scope
+=====
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+Enforcement
+===========
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at [email protected]. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+Attribution
+===========
+
+This Code of Conduct is adapted from the `Contributor Covenant <https://contributor-covenant.org>`_, version 1.4,
+available `here <https://contributor-covenant.org/version/1/4>`_.
+

+ 72 - 0
docs/_build/html/_sources/external-clients.rst.txt

@@ -0,0 +1,72 @@
+================
+External Clients
+================
+
+Introduction
+===============
+
+Netmaker allows for "external clients" to reach into a network and access services via an Ingress Gateway. So what is an "external client"? An external client is any machine which cannot or should not be meshed. This can include:
+        - Phones
+        - Laptops
+        - Desktops
+
+An external client is not "managed," meaning it does not automatically pull the latest network configuration, or push changes to its configuration. Instead, it uses a generated WireGuard config file to access the designated **Ingress Gateway**, which **is** a managed server (running netclient). This server then forwards traffic to the appropriate endpoint, acting as a middle-man/relay.
+
+By using this method, you can hook any machine into a netmaker network that can run WireGuard.
+
+It is recommended to run the netclient where compatible, but for all other cases, a machine can be configured as an external client.
+
+Important to note, an external client is not **reachable** by the network, meaning the client can establish connections to other machines, but those machines cannot independently establish a connection back. The External Client method should only be used in use cases where one wishes to access resource running on the virtual network, and **not** for use cases where one wishes to make a resource accessible on the network. For that, use netclient.
+
+Configuring an Ingress Gateway
+==================================
+
+External Clients must attach to an Ingress Gateway. By default, your network will not have an ingress gateway. To configure an ingress gateway, you can use any node in your network, but it should have a public IP address (not behind a NAT). Your Netmaker server can be an ingress gateway and makes for a good default choice if you are unsure of which node to select.
+
+.. image:: images/exclient1.png
+   :width: 80%
+   :alt: Gateway
+   :align: center
+
+Adding Clients to a Gateway
+=============================
+
+Once you have configured a node as a gateway, you can then add clients to that gateway. Clients will be able to access other nodes in the network just as the gateway node does.
+
+.. image:: images/exclient2.png
+   :width: 80%
+   :alt: Gateway
+   :align: center
+
+After creating a client, you can edit the name to something more logical.
+
+.. image:: images/exclient3.png
+   :width: 80%
+   :alt: Gateway
+   :align: center
+
+Then, you can either download the configuration file directly, or scan the QR code from your phone (assuming you have the WireGuard app installed). It will accept the configuration just as it would accept a typical WireGuard configuration file.
+
+.. image:: images/exclient4.png
+   :width: 80%
+   :alt: Gateway
+   :align: center
+
+Example config file: 
+
+.. literalinclude:: ./examplecode/myclient.conf
+
+Your client should now be able to access the network! A client can be invalidated at any time by simply deleting it from the UI.
+
+Configuring DNS for Ext Clients (OPTIONAL)
+============================================
+
+If you wish to have a DNS field on your ext clients conf, simply edit the network field as shown below to 1.1.1.1 or 8.8.8.8 for example.
+If you do not want DNS on your ext client conf files, simply leave it blank.
+
+.. image:: images/extclient5.png
+   :width: 80%
+   :alt: Gateway
+   :align: center
+
+Important to note, your client automatically adds egress gateway ranges (if any on the same network) to it's allowed IPs.

+ 127 - 0
docs/_build/html/_sources/getting-started.rst.txt

@@ -0,0 +1,127 @@
+=================
+Getting Started
+=================
+
+Once you have Netmaker installed via the :doc:`Quick Install <./quick-start>` guide, you can use this Getting Started guide to help create and manage your first network.
+
+Setup
+=================
+
+#. Create your admin user, with a username and password.
+#. Login with your new user
+#. Create your first network by clicking on Create Network
+
+.. image:: images/create-net.png
+   :width: 80%
+   :alt: Create Network Screen
+   :align: center
+
+This network should have a sensible name (nodes will use it to set their interfaces).
+
+More importantly, it should have a non-overlapping, private address range. 
+
+If you are running a small (less than 254 machines) network, and are unsure of which CIDR's to use, you could consider:
+
+- 10.11.12.0/24
+- 10.20.30.0/24
+- 100.99.98.0/24
+
+Once your network is created, you should see that the netmaker server has added itself to the network. From here, you can move on to adding additional nodes to the network.
+
+.. image:: images/netmaker-node.png
+   :width: 80%
+   :alt: Node Screen
+   :align: center
+
+
+Create Key
+------------
+
+Adding nodes to the network typically requires a key.
+
+#. Click on the ACCESS KEYS tab and select the network you created.
+#. Click ADD NEW ACCESS KEY
+#. Give it a name (ex: "mykey") and a number of uses (ex: 25)
+#. Click CREATE KEY (**Important:** Do not click out of the following screen until you have saved your key details. It will appear only once.)
+#. Copy the bottom command under "Your agent install command with access token" and save it somewhere locally. E.x: ``curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/netclient-install.sh | KEY=vm3ow4thatogiwnsla3thsl3894ths sh -``.
+
+.. image:: images/access-key.png
+   :width: 80%
+   :alt: Access Key Screen
+   :align: center
+
+You will use this command to install the netclient on your nodes. There are three different values for three different scenarios: 
+
+* The **Access Key** value is the secret string that will allow your node to authenticate with the Netmaker network. This can be used with existing netclient installations where additional configurations (such as setting the server IP manually) may be required. This is not typical. E.g. ``netclient join -k <access key> -s grpc.myserver.com -p 50051``
+* The **Access Token** value is a base64 encoded string that contains the server IP and grpc port, as well as the access key. This is decoded by the netclient and can be used with existing netclient installations like this: ``netclient join -t <access token>``. You should use this method for adding a network to a node that is already on a network. For instance, Node A is in the **mynet** network and now you are adding it to **default**.
+* The **install command** value is a curl command that can be run on Linux systems. It is a simple script that downloads the netclient binary and runs the install command all in one.
+  
+Networks can also be enabled to allow nodes to sign up without keys at all. In this scenario, nodes enter a "pending state" and are not permitted to join the network until an admin approves them.
+
+Deploy Nodes
+=================
+
+0. Prereqisite: Every machine on which you install should have wireguard and systemd already installed.
+
+1. SSH to each machine 
+2. ``sudo su -``
+3. **Prerequisite Check:** Every Linux machine on which you run the netclient must have WireGuard and systemd installed
+4. For linux machines with SystemD and WireGuard installed, Run the install command, Ex: ``curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/netclient-install.sh | KEY=vm3ow4thatogiwnsla3thsl3894ths sh -``
+5. For Mac, Windows, and arch-specific linux distributions (e.g. ARM), `download the appropriate netclient for your system <https://github.com/gravitl/netmaker/releases/tag/latest/>`_ . Then, run "netclient join -t <your token>".
+
+You should get output similar to the below. The netclient retrieves local settings, submits them to the server for processing, and retrieves updated settings. Then it sets the local network configuration. For more information about this process, see the :doc:`client installation <./client-installation>` documentation. If this process failed and you do not see your node in the console (see below), then reference the :doc:`troubleshooting <./troubleshoot>` documentation.
+
+.. image:: images/nc-install-output.png
+   :width: 80%
+   :alt: Output from Netclient Install
+   :align: center
+
+
+.. image:: images/nm-node-success.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+
+Repeat the above steps for every machine you would like to add to your network. You can re-use the same install command so long as you do not run out of uses on your access key (after which it will be invalidated and deleted).
+
+Once installed on all nodes, you can test the connection by pinging the private address of any node from any other node.
+
+
+.. image:: images/ping-node.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+Manage Nodes
+===============
+
+Your machines should now be visible in the control pane. 
+
+.. image:: images/nodes.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+You can view/modify/delete any node by selecting it in the NODES tab. For instance, you can change the name to something more sensible like "workstation" or "api server". You can also modify network settings here, such as keys or the WireGuard port. These settings will be picked up by the node on its next check in. For more information, see Advanced Configuration in the :doc:`Using Netmaker <./usage>` docs.
+
+.. image:: images/node-details.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+
+
+Nodes can be added/removed/modified on the network at any time. Nodes can also be added to multiple Netmaker networks. Any changes will get picked up by any nodes on a given network, and will take aboue ~30 seconds to take effect.
+
+Uninstalling the netclient
+=============================
+
+1. To remove your nodes from the default network, run the following on each node: ``sudo netclient leave -n default``
+2. To remove the netclient entirely from each node, run ``sudo rm -rf /etc/netclient`` (after running the first step)
+
+Uninstalling Netmaker
+===========================
+
+To uninstall Netmaker from the server, simply run ``docker-compose down`` or ``docker-compose down --volumes`` to remove the docker volumes for a future installation.
+

+ 186 - 0
docs/_build/html/_sources/index.rst.txt

@@ -0,0 +1,186 @@
+.. Netmaker documentation master file, created by
+   sphinx-quickstart on Fri May 14 08:51:40 2021.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+
+.. image:: images/netmaker.png
+   :width: 100%
+   :alt: Netmaker WireGuard
+   :align: center
+
+.. role:: raw-html(raw)
+    :format: html
+
+:raw-html:`<br />`
+
+=======================================
+Welcome to the Netmaker Documentation
+=======================================
+
+
+Netmaker is a platform for creating and managing fast, secure, and dynamic virtual overlay networks using WireGuard.
+
+This documentation covers Netmaker's :doc:`installation <./server-installation>`, :doc:`usage <./usage>`, :doc:`troubleshooting <./support>`, and customization, as well as reference documents for the :doc:`API <./api>`, UI and Agent configuration. All of the `source code <https://github.com/gravitl/netmaker>`_ for Netmaker is on GitHub.
+
+
+.. :raw-html:`<br />`
+
+.. .. raw:: html
+..   :file: youtube-1.html
+
+About
+------
+A quick overview of Netmaker, explaining what it is, how it works, and why you should be using it.
+
+.. toctree::
+   :maxdepth: 2
+   
+   about
+
+Architecture
+---------------
+
+A technical overview of Netmaker, including design decisions and limitations.
+
+.. toctree::
+   :maxdepth: 2
+   
+   architecture
+
+Install
+------------------------------------
+
+Choose the right install method for you.
+
+.. toctree::
+   :maxdepth: 1
+
+   install
+
+Quick Start
+---------------
+
+A quick start guide to getting up and running with Netmaker and WireGuard as quickly as possible.
+
+.. toctree::
+   :maxdepth: 2
+
+   quick-start
+
+.. toctree::
+   :maxdepth: 2
+
+   getting-started
+
+Quick Start Nginx (depreciated)
+------------------------------------
+
+An older guide to getting up and running with Netmaker using Nginx as quickly as possible.
+
+.. toctree::
+   :maxdepth: 1
+
+   quick-start-nginx
+
+Server Installation
+--------------------
+
+A detailed guide to installing the Netmaker server (API, DB, UI, DNS), and configuration options.
+
+.. toctree::
+   :maxdepth: 2
+   
+   server-installation
+
+Oauth Configuration
+--------------------
+
+A simple guide to configuring OAuth for Netmaker.
+
+.. toctree::
+   :maxdepth: 2
+   
+   oauth
+
+
+Client Installation
+--------------------
+
+A detailed guide to installing the Netmaker agent (netclient) on devices and configuration options.
+
+.. toctree::
+   :maxdepth: 2
+   
+   client-installation
+
+External Clients
+--------------------
+
+A detailed guide to give clients outside of the Netmaker network access to network resources.
+
+.. toctree::
+   :maxdepth: 2
+   
+   external-clients
+
+Guides
+----------------
+
+A handful of guides for use cases including site-to-site, Kubernetes, private DNS, and more.
+
+.. toctree::
+   :maxdepth: 2
+   
+   usage
+
+API Reference
+---------------
+
+A reference document for the Netmaker Server API, and example API calls for various use cases.
+
+**Coming Soon:** Swagger Documentation
+
+.. toctree::
+   :maxdepth: 1
+
+   api
+
+Troubleshooting
+----------------
+
+Help with common Netmaker/netclient issues.
+
+.. toctree::
+   :maxdepth: 2
+
+   troubleshoot
+
+
+Support
+----------------
+
+Where to go for help, and a FAQ.
+
+.. toctree::
+   :maxdepth: 2
+
+   support
+
+Code of Conduct
+-----------------
+
+A statement on our expectations and pledge to the community.
+
+.. toctree:: 
+
+        conduct.rst
+
+Licensing
+---------------
+
+A link to the Netmaker license.
+
+.. toctree:: 
+
+        license.rst

+ 20 - 0
docs/_build/html/_sources/install.rst.txt

@@ -0,0 +1,20 @@
+=========
+Install
+=========
+
+Choose the install method that makes sense for you.
+
+**For most users, we recommend the** :doc:`Quick Install<./quick-start>` **guide.**
+
+`Trial, PoC, Testing, and Experimenting <https://github.com/gravitl/netmaker/tree/master#get-started-in-5-minutes>`_
+
+:doc:`Quick Install: for general small-to-medium use cases <./quick-start>`
+
+:ref:`Kubernetes Installation <KubeInstall>`
+
+:ref:`Non-Docker (from binary) Install <NoDocker>`
+
+:ref:`Highly Available Installation <HAInstall>`
+
+:doc:`Advanced Install Resources <./server-installation>`
+

+ 6 - 0
docs/_build/html/_sources/license.rst.txt

@@ -0,0 +1,6 @@
+=======
+License
+=======
+
+Netmaker's source code and all artifacts in this repository are freely available. All versions are published under the Server Side Public License (SSPL), version 1, which can be found `here <https://raw.githubusercontent.com/gravitl/netmaker/master/LICENSE.txt>`_.
+

+ 81 - 0
docs/_build/html/_sources/oauth.rst.txt

@@ -0,0 +1,81 @@
+====================
+Integrating OAuth
+====================
+
+Introduction
+==============
+
+As of v0.8.5, Netmaker offers integration with the following OAuth providers: 
+
+- GitHub
+- Google
+- Microsoft Azure AD
+
+By integrating with an OAuth provider, your Netmaker users can log in via the provider, rather than the default simple auth.
+
+Configuring your provider
+===========================
+
+In order to use OAuth, configure your OAuth provider (GitHub, Google, Azure AD).
+
+You must configure your provider (except for Azure AD) to use the Netmaker Dashboard URI dashboard.<netmaker.base.domain> as the origin URL.
+
+For example: `https://dashboard.netmaker.mydomain.com`
+
+You must configure your provider to use the Netmaker API URI redirect route with the following format: https://api.<netmaker base domain>/api/oauth/callback.
+
+For example: `https://api.netmaker.mydomain.com/api/oauth/callback`
+
+General provider instructions can be found with the following links:
+
+Instructions for GitHub: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#github-auth-provider
+Instructions for Google: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#google-auth-provider
+Instructions for Microsoft Azure AD: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#microsoft-azure-ad-provider 
+
+Configuring Netmaker
+======================
+
+After you have configured your OAuth provider, take note of the CLIENT_ID and CLIENT_SECRET.
+
+Next, Configure Netmaker with the following environment variables. If any are left blank, OAuth will fail.
+
+.. code-block::
+
+    AUTH_PROVIDER: "<azure-ad|github|google>"
+    CLIENT_ID: "<client id of your oauth provider>"
+    CLIENT_SECRET: "<client secret of your oauth provider>"
+    SERVER_HTTP_HOST: "api.<netmaker base domain>"
+    FRONTEND_URL: "https://dashboard.<netmaker base domain>"
+
+
+After restarting your server, the Netmaker logs will indicate if the OAuth provider was successfully initialized:
+
+.. code-block::
+
+   sudo docker logs netmaker
+
+Once successful, users can click the key symbol on the login page to sign-in with your configured OAuth provider.
+
+.. image:: images/oauth1.png
+   :width: 80%
+   :alt: Login Oauth
+   :align: center
+
+Configuring User Permissions
+===============================
+
+All users logging in will have zero permissions on first sign-in. An admin must configure all user permissions.
+
+Admins must navigate to the "Users" screen to configure permissions.
+
+For each user, an admin must specify which networks that user has access to configure. Additionally, an Admin can elevate a user to Admin permissions.
+
+.. image:: images/oauth3.png
+   :width: 80%
+   :alt: Edit User 2
+   :align: center
+
+.. image:: images/oauth2.png
+   :width: 80%
+   :alt: Edit User
+   :align: center

+ 170 - 0
docs/_build/html/_sources/quick-start-nginx.rst.txt

@@ -0,0 +1,170 @@
+==================================
+Install with Nginx (depreciated)
+==================================
+
+This is the old quick start guide, which contains instructions using Nginx and Docker CE. It is recommended to use the new quick start guide with Caddy instead.
+
+0. Introduction
+==================
+
+We assume for this installation that you want all of the Netmaker features enabled, you want your server to be secure, and you want your server to be accessible from anywhere.
+
+This instance will not be HA. However, it should comfortably handle around one hundred concurrent clients and support the most common use cases.
+
+If you are deploying for a business or enterprise use case and this setup will not fit your needs, please contact [email protected], or check out the business subscription plans at https://gravitl.com/plans/business.
+
+By the end of this guide, you will have Netmaker installed on a public VM linked to your custom domain, secured behind an Nginx reverse proxy.
+
+For information about deploying more advanced configurations, see the :doc:`Advanced Installation <./server-installation>` docs. 
+
+
+1. Prerequisites
+==================
+-  **Virtual Machine**
+   
+   - Preferably from a cloud provider (e.x: DigitalOcean, Linode, AWS, GCP, etc.)
+      - We do not recommend Oracle Cloud, as VM's here have been known to cause network interference.
+   - Public, static IP 
+   - Min 1GB RAM, 1 CPU (4GB RAM, 2CPU preferred)
+      - Nginx may have performance issues if using a cloud VPS with a single, shared CPU
+   - 2GB+ of storage 
+   - Ubuntu  20.04 Installed
+
+- **Domain**
+
+  - A publicly owned domain (e.x. example.com, mysite.biz) 
+  - Permission and access to modify DNS records via DNS service (e.x: Route53)
+
+2. Install Dependencies
+========================
+
+``ssh root@your-host``
+
+Install Docker
+---------------
+Begin by installing the community version of Docker and docker-compose (there are issues with the snap version). You can follow the official `Docker instructions here <https://docs.docker.com/engine/install/>`_. Or, you can use the below series of commands which should work on Ubuntu 20.04.
+
+.. code-block::
+
+  sudo apt-get remove docker docker-engine docker.io containerd runc
+  sudo apt-get update
+  sudo apt-get -y install apt-transport-https ca-certificates curl gnupg lsb-release
+  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg  
+  echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+  sudo apt-get update
+  sudo apt-get -y install docker-ce docker-ce-cli containerd.io
+  sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+  sudo chmod +x /usr/local/bin/docker-compose
+  docker --version
+  docker-compose --version
+
+At this point Docker should be installed.
+
+Install Dependencies
+-----------------------------
+
+In addition to Docker, this installation requires WireGuard, Nginx, and Certbot.
+
+``sudo apt -y install wireguard wireguard-tools nginx certbot python3-certbot-nginx net-tools``
+
+ 
+3. Prepare VM
+===============================
+
+Prepare Domain
+----------------------------
+1. Choose a base domain or subdomain for Netmaker. If you own **example.com**, this should be something like **netmaker.example.com**
+
+- You must point your wildcard domain to the public IP of your VM, e.x: *.example.com --> <your public ip>
+
+2. Add an A record pointing to your VM using your DNS service provider for *.netmaker.example.com (inserting your own subdomain of course).
+3. Netmaker will create three subdomains on top of this. For the example above those subdomains would be:
+
+- dashboard.netmaker.example.com
+
+- api.netmaker.example.com
+
+- grpc.netmaker.example.com
+
+Moving forward we will refer to your base domain using **<your base domain>**. Replace these references with your domain (e.g. netmaker.example.com).
+
+4. ``nslookup host.<your base domain>`` (inserting your domain) should now return the IP of your VM.
+
+5. Generate SSL Certificates using certbot:
+
+``sudo certbot certonly --manual --preferred-challenges=dns --email [email protected] --server https://acme-v02.api.letsencrypt.org/directory --agree-tos --manual-public-ip-logging-ok -d "*.<your base domain>"``
+
+The above command (using your domain instead of <your base domain>), will prompt you to enter a TXT record in your DNS service provider. Do this, and **wait one  minute** before clicking enter, or it may fail and you will have to run the command again.
+
+Prepare Firewall
+-----------------
+
+Make sure firewall settings are appropriate for Netmaker. You need ports 53 and 443. On the server you can run:
+
+
+.. code-block::
+
+  sudo ufw allow proto tcp from any to any port 443 && sudo ufw allow 53/udp && sudo ufw allow 53/tcp
+
+**Based on your cloud provider, you may also need to set inbound security rules for your server. This will be dependent on your cloud provider. Be sure to check before moving on:**
+  - allow 443/tcp from all
+  - allow 53/udp and 53/tcp from all
+
+In addition to the above ports, you will need to make sure that your cloud's firewall or security groups are opened for the range of ports that Netmaker's WireGuard interfaces consume.
+
+Netmaker will create one interface per network, starting from 51821. So, if you plan on having 5 networks, you will want to have at least 51821-51825 open (udp).
+
+Prepare Nginx
+-----------------
+
+Nginx will serve the SSL certificate with your chosen domain and forward traffic to netmaker.
+
+Get the nginx configuration file:
+
+``wget https://raw.githubusercontent.com/gravitl/netmaker/master/nginx/netmaker-nginx-template.conf``
+
+Insert your domain in the configuration file and add to nginx:
+
+.. code-block::
+
+  sed -i 's/NETMAKER_BASE_DOMAIN/<your base domain>/g' netmaker-nginx-template.conf
+  sudo cp netmaker-nginx-template.conf /etc/nginx/conf.d/<your base domain>.conf
+  nginx -t && nginx -s reload
+  systemctl restart nginx
+
+4. Install Netmaker
+====================
+
+Prepare Templates
+------------------
+
+**Note on COREDNS_IP:** Depending on your cloud provider, the public IP may not be bound directly to the VM on which you are running. In such cases, CoreDNS cannot bind to this IP, and you should use the IP of the default interface on your machine in place of COREDNS_IP. If the public IP **is** bound to the VM, you can simply use the same IP as SERVER_PUBLIC_IP.
+
+.. code-block::
+
+  wget https://raw.githubusercontent.com/gravitl/netmaker/master/compose/docker-compose.yml
+  sed -i 's/NETMAKER_BASE_DOMAIN/<your base domain>/g' docker-compose.yml
+  sed -i 's/SERVER_PUBLIC_IP/<your server ip>/g' docker-compose.yml
+  sed -i 's/COREDNS_IP/<your server ip>/g' docker-compose.yml
+
+Generate a unique master key and insert it:
+
+.. code-block::
+
+  tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo ''
+  sed -i 's/REPLACE_MASTER_KEY/<your generated key>/g' docker-compose.yml
+
+You may want to save this key for future use with the API.
+
+Start Netmaker
+----------------
+
+``sudo docker-compose -f docker-compose.yml up -d``
+
+navigate to dashboard.<your base domain> to log into the UI.
+
+To troubleshoot issues, start with:
+
+``docker logs netmaker``
+
+Or check out the :doc:`troubleshoooting docs <./troubleshoot>`.

+ 141 - 0
docs/_build/html/_sources/quick-start.rst.txt

@@ -0,0 +1,141 @@
+===============
+Quick Install
+===============
+
+This quick start guide is an **opinionated** guide for getting up and running with Netmaker as quickly as possible.
+
+If just trialing netmaker, you may also want to check out the 3-minute PoC install of Netmaker in the README on GitHub. The following is just a guided version of that script, plus a custom domain (instead of nip.io): https://github.com/gravitl/netmaker .
+
+Introduction
+==================
+
+We assume for this installation that you want all of the Netmaker features enabled, you want your server to be secure, and you want your server to be accessible from anywhere.
+
+This instance will not be HA. However, it should comfortably handle around one hundred concurrent clients and support the most common use cases.
+
+If you are deploying for a business or enterprise use case and this setup will not fit your needs, please contact [email protected], or check out the business subscription plans at https://gravitl.com/plans/business.
+
+By the end of this guide, you will have Netmaker installed on a public VM linked to your custom domain, secured behind a Caddy reverse proxy.
+
+For information about deploying more advanced configurations, see the :doc:`Advanced Installation <./server-installation>` docs. 
+
+
+0. Prerequisites
+==================
+-  **Virtual Machine**
+   
+   - Preferably from a cloud provider (e.x: DigitalOcean, Linode, AWS, GCP, etc.)
+   
+   - (We do not recommend Oracle Cloud, as VM's here have been known to cause network interference.)
+
+   - Public, static IP 
+   
+   - Min 1GB RAM, 1 CPU (4GB RAM, 2CPU preferred for production installs)
+   
+   - 2GB+ of storage 
+   
+   - Ubuntu  20.04 Installed
+
+- **Domain**
+
+  - A publicly owned domain (e.x. example.com, mysite.biz) 
+  - Permission and access to modify DNS records via DNS service (e.x: Route53)
+
+1. Prepare DNS
+================
+
+Create a wildcard A record pointing to the public IP of your VM. As an example, *.netmaker.example.com.
+
+Caddy will create 3 subdomains with this wildcard, EX:
+
+- dashboard.netmaker.example.com
+
+- api.netmaker.example.com
+
+- grpc.netmaker.example.com
+
+
+2. Install Dependencies
+========================
+
+.. code-block::
+
+  ssh root@your-host
+  sudo apt-get update
+  sudo apt-get install -y docker.io docker-compose wireguard
+
+At this point you should have all the system dependencies you need.
+ 
+3. Open Firewall
+===============================
+
+Make sure firewall settings are set for Netmaker both on the VM and with your cloud security groups (AWS, GCP, etc). 
+
+Make sure the following ports are open both on the VM and in the cloud security groups:
+
+- **443 (tcp):** for Dashboard, REST API, and gRPC
+- **53 (udp and tcp):** for CoreDNS
+- **51821-518XX (udp):** for WireGuard - Netmaker needs one port per network, starting with 51821, so open up a range depending on the number of networks you plan on having. For instance, 51821-51830.
+
+.. code-block::
+
+  sudo ufw allow proto tcp from any to any port 443 && sudo ufw allow 53/udp && sudo ufw allow 53/tcp && sudo ufw allow 51821:51830/udp
+
+**Again, based on your cloud provider, you may additionally need to set inbound security rules for your server (for instance, on AWS). This will be dependent on your cloud provider. Be sure to check before moving on:**
+  - allow 443/tcp from all
+  - allow 53/udp and 53/tcp from all
+  - allow 51821-51830/udp from all
+
+
+4. Install Netmaker
+========================
+
+Prepare Docker Compose 
+------------------------
+
+**Note on COREDNS_IP:** Depending on your cloud provider, the public IP may not be bound directly to the VM on which you are running. In such cases, CoreDNS cannot bind to this IP, and you should use the IP of the default interface on your machine in place of COREDNS_IP. This command will get you the correct IP for CoreDNS in many cases:
+
+.. code-block::
+
+  ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p'
+
+Now, insert the values for your base (wildcard) domain, public ip, and coredns ip.
+
+.. code-block::
+
+  wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/compose/docker-compose.contained.yml
+  sed -i 's/NETMAKER_BASE_DOMAIN/<your base domain>/g' docker-compose.yml
+  sed -i 's/SERVER_PUBLIC_IP/<your server ip>/g' docker-compose.yml
+  sed -i 's/COREDNS_IP/<default interface ip>/g' docker-compose.yml
+
+Generate a unique master key and insert it:
+
+.. code-block::
+
+  tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo ''
+  sed -i 's/REPLACE_MASTER_KEY/<your generated key>/g' docker-compose.yml
+
+You may want to save this key for future use with the API.
+
+Prepare Caddy
+------------------------
+
+.. code-block::
+
+  wget -O /root/Caddyfile https://raw.githubusercontent.com/gravitl/netmaker/master/docker/Caddyfile
+
+  sed -i 's/NETMAKER_BASE_DOMAIN/<your base domain>/g' /root/Caddyfile
+  sed -i 's/YOUR_EMAIL/<your email>/g' /root/Caddyfile
+
+Start Netmaker
+----------------
+
+``sudo docker-compose up -d``
+
+navigate to dashboard.<your base domain> to begin using Netmaker.
+
+To troubleshoot issues, start with:
+
+``docker logs netmaker``
+
+Or check out the :doc:`troubleshoooting docs <./troubleshoot>`.

+ 571 - 0
docs/_build/html/_sources/server-installation.rst.txt

@@ -0,0 +1,571 @@
+=================================
+Advanced Server Installation
+=================================
+
+This section outlines installing the Netmaker server, including Netmaker, Netmaker UI, rqlite, and CoreDNS
+
+System Compatibility
+====================
+
+Netmaker will require elevated privileges to perform network operations. Netmaker has similar limitations to :doc:`netclient <./client-installation>` (client networking agent). 
+
+Typically, Netmaker is run inside of containers (Docker). To run a non-docker installation, you must run the Netmaker binary, CoreDNS binary, database, and a web server directly on the host. Each of these components have their own individual requirements.
+
+The quick install guide is recommended for first-time installs. 
+
+The following documents are meant for special cases like Kubernetes and LXC, or for more advanced setups. 
+
+
+Server Configuration Reference
+==========================================
+
+Netmaker sets its configuration in the following order of precendence:
+
+1. Defaults
+2. Config File
+3. Environment Variables
+
+Variable Description
+----------------------
+VERBOSITY:
+    **Default:** 0
+
+    **Description:** Specify level of logging you would like on the server. Goes up to 3 for debugging.
+
+
+GRPC_SSL:
+    **Default:** "off"
+
+    **Description:** Specifies if GRPC is going over secure GRPC or SSL. This is a setting for the clients and is passed through the access token. Can be set to "on" and "off". Set to on if SSL is configured for GRPC.
+
+SERVER_API_CONN_STRING
+    **Default:** ""
+
+    **Description:**  Allows specification of the string used to connect to the server api. Format: IP:PORT or DOMAIN:PORT. Defaults to SERVER_HOST if not specified.
+
+SERVER_GRPC_CONN_STRING
+    **Default:** ""
+
+    **Description:**  Allows specification of the string used to connect to grpc. Format: IP:PORT or DOMAIN:PORT. Defaults to SERVER_HOST if not specified.
+
+SERVER_HOST: *(depreciated, use SERVER_API_CONN_STRING and SERVER_GRPC_CONN_STRING)* 
+    **Default:** Server will perform an IP check and set automatically unless explicitly set, or DISABLE_REMOTE_IP_CHECK is set to true, in which case it defaults to 127.0.0.1
+
+    **Description:** Sets the SERVER_HTTP_HOST and SERVER_GRPC_HOST variables if they are unset. The address where traffic comes in. 
+
+SERVER_HTTP_HOST: *(depreciated, use SERVER_API_CONN_STRING and SERVER_GRPC_CONN_STRING)*
+    **Default:** Equals SERVER_HOST if set, "127.0.0.1" if SERVER_HOST is unset.
+    
+    **Description:** Set to make the HTTP and GRPC functions available via different interfaces/networks.
+
+SERVER_GRPC_HOST: *(depreciated, use SERVER_API_CONN_STRING and SERVER_GRPC_CONN_STRING)*
+    **Default:** Equals SERVER_HOST if set, "127.0.0.1" if SERVER_HOST is unset.
+
+    **Description:** Set to make the HTTP and GRPC functions available via different interfaces/networks.
+
+API_PORT:
+    **Default:** 8081 
+
+    **Description:** The HTTP API port for Netmaker. Used for API calls / communication from front end.
+
+GRPC_PORT:  
+    **Default:** 50051
+
+    **Description:** The GRPC port for Netmaker. Used for communications from nodes.
+
+MASTER_KEY:  
+    **Default:** "secretkey" 
+
+    **Description:** The admin master key for accessing the API. Change this in any production installation.
+
+CORS_ALLOWED_ORIGIN:  
+    **Default:** "*"
+
+    **Description:** The "allowed origin" for API requests. Change to restrict where API requests can come from.
+
+REST_BACKEND:  
+    **Default:** "on" 
+
+    **Description:** Enables the REST backend (API running on API_PORT at SERVER_HTTP_HOST). Change to "off" to turn off.
+
+AGENT_BACKEND:  
+    **Default:** "on" 
+
+    **Description:** Enables the AGENT backend (GRPC running on GRPC_PORT at SERVER_GRPC_HOST). Change to "off" to turn off.
+
+DNS_MODE:  
+    **Default:** "off"
+
+    **Description:** Enables DNS Mode, meaning config files will be generated for CoreDNS.
+
+DATABASE:  
+    **Default:** "sqlite"
+
+    **Description:** Specify db type to connect with. Currently, options include "sqlite", "rqlite", and "postgres".
+
+SQL_CONN:
+    **Default:** "http://"
+
+    **Description:** Specify the necessary string to connect with your local or remote sql database.
+
+SQL_HOST:
+    **Default:** "localhost"
+
+    **Description:** Host where postgres is running.
+
+SQL_PORT:
+    **Default:** "5432"
+
+    **Description:** port postgres is running.
+
+SQL_DB:
+    **Default:** "netmaker"
+
+    **Description:** DB to use in postgres.
+
+SQL_USER:
+    **Default:** "postgres"
+
+    **Description:** User for posgres.
+
+SQL_PASS:
+    **Default:** "nopass"
+
+    **Description:** Password for postgres.
+
+CLIENT_MODE:  
+    **Default:** "on"
+
+    **Description:** Specifies if server should deploy itself as a node (client) in each network. May be turned to "off" for more restricted servers.
+
+Config File Reference
+----------------------
+A config file may be placed under config/environments/<env-name>.yml. To read this file at runtime, provide the environment variable NETMAKER_ENV at runtime. For instance, dev.yml paired with ENV=dev. Netmaker will load the specified Config file. This allows you to store and manage configurations for different environments. Below is a reference Config File you may use.
+
+.. literalinclude:: ../config/environments/dev.yaml
+  :language: YAML
+
+Compose File - Annotated
+--------------------------------------
+
+All environment variables and options are enabled in this file. It is the equivalent to running the "full install" from the above section. However, all environment variables are included, and are set to the default values provided by Netmaker (if the environment variable was left unset, it would not change the installation). Comments are added to each option to show how you might use it to modify your installation.
+
+.. literalinclude:: ../compose/docker-compose.reference.yml
+  :language: YAML
+
+
+DNS Mode Setup
+====================================
+
+If you plan on running the server in DNS Mode, know that a `CoreDNS Server <https://coredns.io/manual/toc/>`_ will be installed. CoreDNS is a light-weight, fast, and easy-to-configure DNS server. It is recommended to bind CoreDNS to port 53 of the host system, and it will do so by default. The clients will expect the nameserver to be on port 53, and many systems have issues resolving a different port.
+
+However, on your host system (for Netmaker), this may conflict with an existing process. On linux systems running systemd-resolved, there is likely a service consuming port 53. The below steps will disable systemd-resolved, and replace it with a generic (e.g. Google) nameserver. Be warned that this may have consequences for any existing private DNS configuration. 
+
+With the latest docker-compose, it is not necessary to perform these steps. But if you are running the install and find that port 53 is blocked, you can perform the following steps, which were tested on Ubuntu 20.04 (these should be run prior to deploying the docker containers).
+
+.. code-block::
+
+  systemctl stop systemd-resolved
+  systemctl disable systemd-resolved 
+  vim /etc/systemd/resolved.conf
+    *  uncomment DNS and add 8.8.8.8 or whatever reachable nameserver is your preference  *
+    *  uncomment DNSStubListener and set to "no"  *
+  ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
+
+Port 53 should now be available for CoreDNS to use.
+
+
+Docker Compose Install
+=======================
+
+The most simple (and recommended) way of installing Netmaker is to use one of the provided `Docker Compose files <https://github.com/gravitl/netmaker/tree/master/compose>`_. Below are instructions for several different options to install Netmaker via Docker Compose, followed by an annotated reference Docker Compose in case your use case requires additional customization.
+
+Test Install - No DNS, No Secure GRPC
+--------------------------------------------------------
+
+This install will run Netmaker on a server without HTTPS using an IP address. This is not secure and not recommended, but can be helpful for testing.
+
+It also does not run the CoreDNS server, to simplify the deployment
+
+**Prerequisites:**
+  * server ports 80, 8081, and 50051 are not blocked by firewall
+
+**Notes:** 
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+.. code-block::
+
+  wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.test.yml
+  sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml
+  docker-compose up -d`
+
+Traefik Proxy
+------------------------
+
+To install with Traefik, rather than Nginx or the default Caddy, check out this repo: https://github.com/bsherman/netmaker-traefik 
+
+
+No DNS - CoreDNS Disabled
+----------------------------------------------
+
+DNS Mode is currently limited to clients that can run resolvectl (systemd-resolved, see :doc:`Architecture docs <./architecture>` for more info). You may wish to disable DNS mode for various reasons. This installation option gives you the full feature set minus CoreDNS.
+
+To run without DNS, follow the :doc:`Quick Install <./quick-start>` guide, omitting the steps for DNS setup. In addition, when the guide has you pull (wget) the Netmaker docker-compose template, use the following link instead:
+
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.nodns.yml``
+
+This template is equivalent but omits CoreDNS.
+
+
+.. _NoDocker:
+
+Linux Install without Docker
+=============================
+
+Most systems support Docker, but some do not. In such environments, there are many options for installing Netmaker. Netmaker is available as a binary file, and there is a zip file of the Netmaker UI static HTML on GitHub. Beyond the UI and Server, you need to install MongoDB and CoreDNS (optional). 
+
+To start, we recommend following the Nginx instructions in the :doc:`Quick Install <./quick-start>` guide to enable SSL for your environment.
+
+Once this is enabled and configured for a domain, you can continue with the below. The recommended server runs Ubuntu 20.04.
+
+rqlite Setup
+----------------
+1. Install rqlite on your server: https://github.com/rqlite/rqlite
+
+2. Run rqlite: rqlited -node-id 1 ~/node.1
+
+Server Setup
+-------------
+1. **Run the install script:** 
+
+``sudo curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/netmaker-server.sh | sh -``
+
+2. Check status:  ``sudo journalctl -u netmaker``
+3. If any settings are incorrect such as host or mongo credentials, change them under /etc/netmaker/config/environments/< your env >.yaml and then run ``sudo systemctl restart netmaker``
+
+UI Setup
+-----------
+
+The following uses Nginx as an http server. You may alternatively use Apache or any other web server that serves static web files.
+
+1. Download and Unzip UI asset files
+2. Copy Config to Nginx
+3. Modify Default Config Path
+4. Change Backend URL
+5. Start Nginx
+
+.. code-block::
+  
+  sudo wget -O /usr/share/nginx/html/netmaker-ui.zip https://github.com/gravitl/netmaker-ui/releases/download/latest/netmaker-ui.zip
+  sudo unzip /usr/share/nginx/html/netmaker-ui.zip -d /usr/share/nginx/html
+  sudo cp /usr/share/nginx/html/nginx.conf /etc/nginx/conf.d/default.conf
+  sudo sed -i 's/root \/var\/www\/html/root \/usr\/share\/nginx\/html/g' /etc/nginx/sites-available/default
+  sudo sh -c 'BACKEND_URL=http://<YOUR BACKEND API URL>:PORT /usr/share/nginx/html/generate_config_js.sh >/usr/share/nginx/html/config.js'
+  sudo systemctl start nginx
+
+CoreDNS Setup
+----------------
+
+.. _KubeInstall:
+
+Kubernetes Install
+=======================
+
+Server Install
+--------------------------
+
+This template assumes your cluster uses Nginx for ingress with valid wildcard certificates. If using an ingress controller other than Nginx (ex: Traefik), you will need to manually modify the Ingress entries in this template to match your environment.
+
+This template also requires RWX storage. Please change references to storageClassName in this template to your cluster's Storage Class.
+
+``wget https://raw.githubusercontent.com/gravitl/netmaker/master/kube/netmaker-template.yaml``
+
+Replace the NETMAKER_BASE_DOMAIN references to the base domain you would like for your Netmaker services (ui,api,grpc). Typically this will be something like **netmaker.yourwildcard.com**.
+
+``sed -i ‘s/NETMAKER_BASE_DOMAIN/<your base domain>/g’ netmaker-template.yaml``
+
+Now, assuming Ingress and Storage match correctly with your cluster configuration, you can install Netmaker.
+
+.. code-block::
+
+  kubectl create ns nm
+  kubectl config set-context --current --namespace=nm
+  kubectl apply -f netmaker-template.yaml -n nm
+
+In about 3 minutes, everything should be up and running:
+
+``kubectl get ingress nm-ui-ingress-nginx``
+
+Netclient Daemonset
+--------------------------
+
+The following instructions assume you have Netmaker running and a network you would like to add your cluster into. The Netmaker server does not need to be running inside of a cluster for this.
+
+.. code-block::
+
+  wget https://raw.githubusercontent.com/gravitl/netmaker/master/kube/netclient-template.yaml
+  sed -i ‘s/ACCESS_TOKEN_VALUE/< your access token value>/g’ netclient-template.yaml
+  kubectl apply -f netclient-template.yaml
+
+For a more detailed guide on integrating Netmaker with MicroK8s, `check out this guide <https://itnext.io/how-to-deploy-a-cross-cloud-kubernetes-cluster-with-built-in-disaster-recovery-bbce27fcc9d7>`_. 
+
+Nginx Reverse Proxy Setup with https
+======================================
+
+The `Swag Proxy <https://github.com/linuxserver/docker-swag>`_ makes it easy to generate a valid ssl certificate for the config bellow. Here is the `documentation <https://docs.linuxserver.io/general/swag>`_ for the installation.
+
+The following file configures Netmaker as a subdomain. This config is an adaption from the swag proxy project.
+
+./netmaker.subdomain.conf:
+
+.. code-block:: nginx
+
+    server {
+        listen 443 ssl;
+        listen [::]:443 ssl;
+
+        server_name netmaker.*; # The external URL
+        client_max_body_size 0;
+
+        # A valid https certificate is needed.
+        include /config/nginx/ssl.conf;
+
+        location / {
+            # This config file can be found at:
+            # https://github.com/linuxserver/docker-swag/blob/master/root/defaults/proxy.conf
+            include /config/nginx/proxy.conf;
+
+            # if you use a custom resolver to find your app, needed with swag proxy
+            # resolver 127.0.0.11 valid=30s;
+            set $upstream_app netmaker-ui;                             # The internal URL
+            set $upstream_port 80;                                     # The internal Port
+            set $upstream_proto http;                                  # the protocol that is being used
+            proxy_pass $upstream_proto://$upstream_app:$upstream_port; # combine the set variables from above
+            }
+        }
+
+    server {
+        listen 443 ssl;
+        listen [::]:443 ssl;
+
+        server_name backend-netmaker.*; # The external URL
+        client_max_body_size 0;
+        underscores_in_headers on;
+
+        # A valid https certificate is needed.
+        include /config/nginx/ssl.conf;
+
+        location / {
+            # if you use a custom resolver to find your app, needed with swag proxy
+            # resolver 127.0.0.11 valid=30s;
+
+            set $upstream_app netmaker;                                # The internal URL
+            set $upstream_port 8081;                                   # The internal Port
+            set $upstream_proto http;                                  # the protocol that is being used
+            proxy_pass $upstream_proto://$upstream_app:$upstream_port; # combine the set variables from above
+
+            # Forces the header to be the one that is visible from the outside
+            proxy_set_header                Host backend.netmaker.example.org; # Please cange to your URL
+
+            # Pass all headers through to the backend
+            proxy_pass_request_headers      on;
+            }
+        }
+
+.. _HAInstall:
+
+
+
+Highly Available Installation (Kubernetes)
+==================================================
+
+Netmaker comes with a Helm chart to deploy with High Availability on Kubernetes:
+
+.. code-block::
+
+    helm repo add netmaker https://gravitl.github.io/netmaker-helm/
+    helm repo update
+
+Requirements
+---------------
+
+To run HA Netmaker on Kubernetes, your cluster must have the following:
+- RWO and RWX Storage Classes (RWX is only required if running Netmaker with DNS Management enabled).
+- An Ingress Controller and valid TLS certificates 
+- This chart can currently generate ingress for Nginx or Traefik Ingress with LetsEncrypt + Cert Manager
+- If LetsEncrypt and CertManager are not deployed, you must manually configure certificates for your ingress
+
+Furthermore, the chart will by default install and use a postgresql cluster as its datastore.
+
+Recommended Settings:
+----------------------
+A minimal HA install of Netmaker can be run with the following command:
+`helm install netmaker --generate-name --set baseDomain=nm.example.com`
+This install has some notable exceptions:
+- Ingress **must** be manually configured post-install (need to create valid Ingress with TLS)
+- Server will use "userspace" WireGuard, which is slower than kernel WG
+- DNS will be disabled
+
+Example Installations:
+------------------------
+An annotated install command:
+
+.. code-block::
+
+    helm install netmaker/netmaker --generate-name \ # generate a random id for the deploy 
+    --set baseDomain=nm.example.com \ # the base wildcard domain to use for the netmaker api/dashboard/grpc ingress 
+    --set replicas=3 \ # number of server replicas to deploy (3 by default) 
+    --set ingress.enabled=true \ # deploy ingress automatically (requires nginx or traefik and cert-manager + letsencrypt) 
+    --set ingress.className=nginx \ # ingress class to use 
+    --set ingress.tls.issuerName=letsencrypt-prod \ # LetsEncrypt certificate issuer to use 
+    --set dns.enabled=true \ # deploy and enable private DNS management with CoreDNS 
+    --set dns.clusterIP=10.245.75.75 --set dns.RWX.storageClassName=nfs \ # required fields for DNS 
+    --set postgresql-ha.postgresql.replicaCount=2 \ # number of DB replicas to deploy (default 2)
+
+
+The below command will install netmaker with two server replicas, a coredns server, and ingress with routes of api.nm.example.com, grpc.nm.example.com, and dashboard.nm.example.com. CoreDNS will be reachable at 10.245.75.75, and will use NFS to share a volume with Netmaker (to configure dns entries).
+
+.. code-block::
+
+    helm install netmaker/netmaker --generate-name --set baseDomain=nm.example.com \
+    --set replicas=2 --set ingress.enabled=true --set dns.enabled=true \
+    --set dns.clusterIP=10.245.75.75 --set dns.RWX.storageClassName=nfs \
+    --set ingress.className=nginx
+
+The below command will install netmaker with three server replicas (the default), **no coredns**, and ingress with routes of api.netmaker.example.com, grpc.netmaker.example.com, and dashboard.netmaker.example.com. There will be one UI replica instead of two, and one database instance instead of two. Traefik will look for a ClusterIssuer named "le-prod-2" to get valid certificates for the ingress. 
+
+.. code-block::
+
+    helm3 install netmaker/netmaker --generate-name \
+    --set baseDomain=netmaker.example.com --set postgresql-ha.postgresql.replicaCount=1 \
+    --set ui.replicas=1 --set ingress.enabled=true \
+    --set ingress.tls.issuerName=le-prod-2 --set ingress.className=traefik
+
+Below, we discuss the considerations for Ingress, Kernel WireGuard, and DNS.
+
+Ingress	
+----------
+To run HA Netmaker, you must have ingress installed and enabled on your cluster with valid TLS certificates (not self-signed). If you are running Nginx as your Ingress Controller and LetsEncrypt for TLS certificate management, you can run the helm install with the following settings:
+
+- `--set ingress.enabled=true`
+- `--set ingress.annotations.cert-manager.io/cluster-issuer=<your LE issuer name>`
+
+If you are not using Nginx or Traefik and LetsEncrypt, we recommend leaving ingress.enabled=false (default), and then manually creating the ingress objects post-install. You will need three ingress objects with TLS:
+
+- `dashboard.<baseDomain>`
+- `api.<baseDomain>`
+- `grpc.<baseDomain>`
+
+If deploying manually, the gRPC ingress object requires special considerations. Look up the proper way to route grpc with your ingress controller. For instance, on Traefik, an IngressRouteTCP object is required.
+
+There are some example ingress objects in the kube/example folder.
+
+Kernel WireGuard
+------------------
+If you have control of the Kubernetes worker node servers, we recommend **first** installing WireGuard on the hosts, and then installing HA Netmaker in Kernel mode. By default, Netmaker will install with userspace WireGuard (wireguard-go) for maximum compatibility, and to avoid needing permissions at the host level. If you have installed WireGuard on your hosts, you should install Netmaker's helm chart with the following option:
+
+- `--set wireguard.kernel=true`
+
+DNS
+----------
+By Default, the helm chart will deploy without DNS enabled. To enable DNS, specify with:
+
+- `--set dns.enabled=true` 
+
+This will require specifying a RWX storage class, e.g.:
+
+- `--set dns.RWX.storageClassName=nfs`
+
+This will also require specifying a service address for DNS. Choose a valid ipv4 address from the service IP CIDR for your cluster, e.g.:
+
+- `--set dns.clusterIP=10.245.69.69`
+
+**This address will only be reachable from hosts that have access to the cluster service CIDR.** It is only designed for use cases related to k8s. If you want a more general-use Netmaker server on Kubernetes for use cases outside of k8s, you will need to do one of the following:
+- bind the CoreDNS service to port 53 on one of your worker nodes and set the COREDNS_ADDRESS equal to the public IP of the worker node
+- Create a private Network with Netmaker and set the COREDNS_ADDRESS equal to the private address of the host running CoreDNS. For this, CoreDNS will need a node selector and will ideally run on the same host as one of the Netmaker server instances.
+
+Values
+---------
+
+To view all options for the chart, please visit the README in the code repo `here <https://github.com/gravitl/netmaker/tree/master/kube/helm#values>`_ .
+
+Highly Available Installation (VMs/Bare Metal)
+==================================================
+
+For an enterprise Netmaker installation, you will need a server that is highly available, to ensure redundant WireGuard routing when any server goes down. To do this, you will need:
+
+1. A load balancer
+2. 3+ Netmaker server instances
+3. rqlite or PostgreSQL as the backing database
+
+These documents outline general HA installation guidelines. Netmaker is highly customizable to meet a wide range of enterprise environments. If you would like support with an enterprise-grade Netmaker installation, you can `schedule a consultation here <https://gravitl.com/book>`_ . 
+
+The main consideration for this document is how to configure rqlite. Most other settings and procedures match the standardized way of making applications HA: Load balancing to multiple instances, and sharing a DB. In our case, the DB (rqlite) is distributed, making HA data more easily achievable.
+
+If using PostgreSQL, follow their documentation for `installing in HA mode <https://www.postgresql.org/docs/14/high-availability.html>`_ and skip step #2.
+
+1. Load Balancer Setup
+------------------------
+
+Your load balancer of choice will send requests to the Netmaker servers. Setup is similar to the various guides we have created for Nginx, Caddy, and Traefik. SSL certificates must also be configured and handled by the LB.
+
+2. RQLite Setup
+------------------
+
+RQLite is the included distributed datastore for an HA Netmaker installation. If you have a different corporate database you wish to integrate, Netmaker is easily extended to other DB's. If this is a requirement, please contact us.
+
+Assuming you use Rqlite, you must run it on each Netmaker server VM, or alongside that VM as a container. Setup a config.json for database credentials (password supports BCRYPT HASHING) and mount in working directory of rqlite and specify with `-auth config.json` :
+
+.. code-block::
+
+    [{
+        "username": "netmaker",
+        "password": "<YOUR_DB_PASSWORD>",
+        "perms": ["all"]
+    }]
+
+
+Once your servers are set up with rqlite, the first instance must be started normally, and then additional nodes must be added with the "join" command. For instance, here is the first server node:
+
+.. code-block::
+
+    sudo docker run -d -p 4001:4001 -p 4002:4002 rqlite/rqlite -node-id 1 -http-addr 0.0.0.0:4001 -raft-addr 0.0.0.0:4002 -http-adv-addr 1.2.3.4:4001 -raft-adv-addr 1.2.3.4:4002 -auth config.json
+
+And here is a joining node:
+
+.. code-block::
+
+    sudo docker run -d -p 4001:4001 -p 4002:4002 rqlite/rqlite -node-id 2 -http-addr 0.0.0.0:4001 -raft-addr 0.0.0.0:4002 -http-adv-addr 2.3.4.5:4001  -raft-adv-addr 2.3.4.5:4002 -join https://netmaker:<YOUR_DB_PASSWORD>@1.2.3.4:4001
+
+- reference for rqlite setup: https://github.com/rqlite/rqlite/blob/master/DOC/CLUSTER_MGMT.md#creating-a-cluster
+- reference for rqlite security: https://github.com/rqlite/rqlite/blob/master/DOC/SECURITY.md
+
+Once rqlite instances have been configured, the Netmaker servers can be deployed.
+
+3. Netmaker Setup
+------------------
+
+Netmaker will be started on each node with default settings, except with DATABASE=rqlite (or DATABASE=postgress) and SQL_CONN set appropriately to reach the local rqlite instance. Rqlite will maintain consistency with each Netmaker backend.
+
+If deploying HA with PostgreSQL, you will connect with the following settings:
+
+.. code-block::
+
+    SQL_HOST = <sql host>
+    SQL_PORT = <port>
+    SQL_DB   = <designated sql DB>
+    SQL_USER = <your user>
+    SQL_PASS = <your password>
+    DATABASE = postgres
+
+
+4. Other Considerations
+------------------------
+
+This is enough to get a functioning HA installation of Netmaker. However, you may also want to make the Netmaker UI or the CoreDNS server HA as well. The Netmaker UI can simply be added to the same servers and load balanced appropriately. For some load balancers, you may be able to do this with CoreDNS as well.
+
+
+
+

+ 61 - 0
docs/_build/html/_sources/support.rst.txt

@@ -0,0 +1,61 @@
+=========
+Support
+=========
+
+FAQ
+======
+
+Does/Will Netmaker Support X Operating System?
+--------------------------------------------------
+
+Netmaker is initially available on a limited number of operating systems for good reason: Every operating system is designed differently. With a small team, we can either focus on making Netmaker do a lot on a few number of operating systems, or a little on a bunch of operating systems. We chose the first option. You can view the System Compatibility docs for more info, but in general, you should only be using Netmaker on systemd linux right now.
+
+However, via "external clients", any device that supports WireGuard can be added to the network. 
+
+In future iterations will expand the operating system support for Netclient, and devices that must use the "external client" feature can switch to Netclient.
+
+How do I install the Netclient on X?
+---------------------------------------
+
+As per the above, there are many unsupported operating systems. You are still welcome to try, it is just an executable binary file after all. If the system is unix-based and has kernel WireGuard installed, netclient may very well mesh the device into the network. However, the service likely will encounter problems retrieving updates.
+
+
+Is Netmaker a VPN like NordNPN?
+--------------------------------
+
+No. Netmaker makes Virtual Networks, which are technically VPNs, but different. It's more like a corporate VPN, or a VPC (if you're familiar with AWS).
+
+If you're looking to achieve self-hosted web browsing, with functionality similar to NordVPN, ExpressVPN, Surfshark, Tunnelbear, or Private Internet Access, this is probably not the project for you. Technically, you can accomplish this with Netmaker, but it would be a little like using a all-terrain vehicle for stock car racing.
+
+There are many good projects out there that support general internet privacy using WireGuard. Here are just a few of them:
+
+https://github.com/trailofbits/algo
+https://github.com/pivpn/pivpn
+https://github.com/subspacecloud/subspace
+https://github.com/mullvad/mullvadvpn-app
+
+Do you offer any enterprise support?
+--------------------------------------
+
+If you are interested in enterprise support for your project, please contact [email protected].
+
+
+Why the SSPL License?
+----------------------
+
+We thought long and hard about the license. Ultimately, we think this is the best way to support and ensure the health of the project long term. The community deserves something that is well-maintained, and in order to do that, eventually we need some financial support. We won't do that by limiting the project, but we will offer some additional support, and hosted options for things people would end up paying for anyway (relay servers, load balancing support, backups). 
+
+While SSPL is not an OSI-approved open source license, it let's people generally run the project however they want, both for private use and business use, without running into the issue of someone else monetizing the project and making it financially untenable. We are working on making the guidelines clear, and will make sure that the license does not impact the communities ability to use and modify the project.
+
+If you have concerns about the license leading to project restrictions down the road, just know that there are other paid, closed-source/closed-core options out there, so beyond not wanting to follow that path, we also don't think it's a good idea economically either. We firmly believe that having the project open is not only right, but the best option.
+
+All that said, we will re-evaluate the license on a regular basis and determine if an OSI-approved license makes more sense. It's just easier to move from SSPL to another license than vice-versa.
+
+
+Contact
+===========
+If you need help, try the discord or open a GitHub ticket.
+
+Email: [email protected]
+
+Discord: https://discord.gg/zRb9Vfhk8A

+ 115 - 0
docs/_build/html/_sources/troubleshoot.rst.txt

@@ -0,0 +1,115 @@
+=================
+Troubleshooting
+=================
+
+Common Issues
+--------------
+**How can I connect my Android or IOS device to my Netmaker VPN?**
+  Currently meshing one of these devices is not supported, however it will be soon. 
+  For now you can connect to your VPN by making one of the nodes an Ingress Gateway, then 
+  create an Ext Client for each device. Finally, use the official WG app or another 
+  WG configuration app to connect via QR or downloading the device's WireGuard configuration. 
+
+**I've made changes to my nodes but the nodes themselves haven't updated yet, why?**
+  Please allow your nodes to complete a check in or two, in order to reconfigure themselves.
+  In some cases, it could take up to a minute or so.
+
+**Do I have to use access keys to join a network?**
+  Although keys are the preferred way to join a network, Netmaker does allow for manual node sign-ups.
+  Simply turn on "allow manual signups" on your network and nodes will not connect until you manually aprove each one.
+
+**Is there a community or forum to ask questions about Netmaker?**
+  Yes, we have an active `discord <https://discord.gg/Pt4T9y9XK8>`_ community and issues on our `github <https://github.com/gravitl/netmaker/issues>`_ are answered frequently!
+  You can also sign-up for updates at our `gravitl site <https://gravitl.com/>`_!
+
+**How can I get additional support for my business?**
+  Check out our business support subscriptions at https://gravitl.com/plans. Subscription holders can also purchase consulting credits via the site.
+
+
+Server
+-------
+
+**I upgraded from 0.7 to 0.8 and now I dont have any data in my server!**
+  In 0.8, sqlite becomes the default database. If you were running with rqlite, you must set the DATABASE environment variable to rqlite in order to continue using rqlite.
+
+**Can I secure/encrypt all the traffic to my server and UI?**
+  This can fairly simple to achieve assuming you have access to a domain and are familiar with Nginx.
+  Please refer to the quick-start guide to see!
+
+**Can I connect multiple nodes (mesh clients) behind a single firewall/router?**
+  Yes! As of version 0.7 Netmaker supports UDP Hole Punching to allow this, without the use of a third party STUN server!
+  Is UDP hole punching a risk for you? Well you can turn it off and make static nodes/ports for the server to refer to as well.
+
+**What are the minimum specs to run the server?**
+  We recommend at least 1 CPU and 2 GB Memory.
+
+**Does this support IPv6 addressing?**
+  Yes, Netmaker supports IPv6 addressing. When you create a network, just make sure to turn on Dual Stack.
+  Nodes will be given IPv6 addresses along with their IPv4 address. It does not currently support IPv6 only.
+
+**Does Netmaker support Raft Consensus?**
+  Netmaker does not directly support it, but it uses `rqlite <https://github.com/rqlite/rqlite>`_ (which supports Raft) as the database.
+
+**How do I uninstall Netmaker?**
+  There is no official uninstall script for the Netmaker server at this time. If you followed the quick-start guide, simply run ``sudo docker-compose -f docker-compose.quickstart.yml down --volumes``
+  to completely wipe your server. Otherwise kill the running binary and it's up to you to remove database records/volumes.
+
+UI
+----
+**I want to make a seperate network and give my friend access to only that network.**
+  Simply navigate to the UI (as an admin account). Select users in the top left and create them an account.
+  Select the network(s) to give them and they should be good to go! They are an admin of that network(s) only now.
+
+**I'm done with an access key, can I delete it?**
+  Simply navigate to the UI (as an admin account). Select your network of interest, then the select the ``Access Keys`` tab.
+  Then delete the rogue access key.
+
+**I can't delete my network, why?**
+  You **MUST** remove all nodes in a network before you can delete it.
+
+**Can I have multiple nodes with the same name?**
+  Yes, nodes can share names without issue. It may just be harder on you to know which is which.
+
+Netclient
+-----------
+**How do I connect a node to my Netmaker network with Netclient?**
+  First get your access token (not just access key), then run ``sudo netclient join -t <access token>``.
+  **NOTE:** netclient may be under /etc/netclient/, i.e run ``sudo /etc/netclient/netclient join -t <access token>``
+
+**How do I disconnect a node on a Netmaker network?**
+  In order to leave a Netmaker network, run ``sudo netclient leave -n <network-name>``
+
+**How do I check the logs of my agent on a node?**
+  You will need sudo/root permissions, but you can run ``sudo systemctl status netclient@<insert network name>``
+  or you may also run ``sudo journalctl -u netclient@<network name>``. 
+  Note for journalctl: you should hit the ``end`` key to get to view the most recent logs quickly or use ``journalctl -u netclient@<network name> -f`` instead.
+
+**Can I check the configuration of my node on the node?**
+  **A:** Yes, on the node simply run ``sudo cat /etc/netclient/netconfig-<network name>`` and you should see what your current configuration is! 
+  You can also see the current WireGuard configuration with ``sudo wg show``
+
+**I am done with the agent on my machine, can I uninstall it?**
+  Yes, on the node simply run ``sudo /etc/netclient/netclient uninstall``. 
+
+**I am running SELinux and when I reboot my node I get a permission denied in my netclient logs and it doesn't connect anymore, why?**
+  If you're running SELinux, it will interfere with systemd's ability to restart the client properly. Therefore, please run the following:
+  .. code-block::
+  
+    sudo semanage fcontext -a -t bin_t '/etc/netclient/netclient' 
+    sudo chcon -Rv -u system_u -t bin_t '/etc/netclient/netclient' 
+    sudo restorecon -R -v /etc/netclient/netclient
+
+**I have a handshake with a peer but can't ping it, what gives?**
+  This is commonly due to incorrect MTU settings. Typically, it will be because MTU is too high. Try setting MTU lower on the node. This can be done via netconfig, or by editing the node in the UI. 
+
+**I have a hard to reach machine behind a firewall or a corporate NAT, what can I do?**
+  In this situation you can use the Relay Server functionality introduced in Netmaker v0.8 to designate a node as a relay to your "stuck" machine. Simply click the button to make a node into a relay and tell it to relay traffic to this hard-to-reach peer. 
+
+
+CoreDNS
+--------
+**Is CoreDNS required to use Netmaker?**
+  CoreDNS is not required. Simply start your server with ``DNS_MODE="off"``.
+
+**What is the minimum DNS entry value I can use?**
+  Netmaker supports down to two characters for DNS names for your networks domains**

+ 24 - 0
docs/_build/html/_sources/usage.rst.txt

@@ -0,0 +1,24 @@
+==============
+Using Netmaker
+==============
+
+Netmaker has many different use cases, from a basic virtual network to an office gateway VPN to a Kubernetes underlay. It can be a bit overwhelming to figure out where to start. If you don't find your use case here, but think Netmaker is a good fit, let us know!
+
+External Tutorials
+==================
+
+Members of the community have created helpful tutorials for getting started with Netmaker. Below are some selected tutorials on different topics.
+
+Video Tutorials
+---------------
+* `Intro/Overview <https://youtu.be/PWLPT320Ybo>`_: Tutorial on first-time usage, setting up a mesh network.
+* `Site-to-Site Gateway <https://youtu.be/krCKBJhwwDk>`_: Tutorial on setting up site-to-site connections, allowing peers to access external networks via gateways.
+* `IPv6 and Private DNS <https://youtu.be/b4diaKWUcXI>`_: Tutorial on dual-stack IPv6 in Netmaker and Private DNS management (separate topics).
+* `Kubernetes Networking <https://youtu.be/z2jvlFVU3dw>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+
+Written Tutorials
+-----------------
+* `K3s Cross-cloud cluster <https://itnext.io/how-to-deploy-a-single-kubernetes-cluster-across-multiple-clouds-using-k3s-and-wireguard-a5ae176a6e81>`_: Tutorial on setting up cross-cloud K3s clusters using Netmaker.
+* `MicroK8s Cross-cloud cluster <https://itnext.io/how-to-deploy-a-cross-cloud-kubernetes-cluster-with-built-in-disaster-recovery-bbce27fcc9d7>`_: Tutorial on setting up cross-cloud MicroK8s clusters using Netmaker.
+* `Secure access to private services <https://afeiszli.medium.com/how-to-enable-secure-access-to-your-hosted-services-using-netmaker-and-wireguard-1b3282d4b7aa>`_: Tutorial on setting up secure Nextcloud with Netmaker.

+ 861 - 0
docs/_build/html/_static/basic.css

@@ -0,0 +1,861 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+    clear: both;
+}
+
+div.section::after {
+    display: block;
+    content: '';
+    clear: left;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+    width: 100%;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: 230px;
+    margin-left: -100%;
+    font-size: 90%;
+    word-wrap: break-word;
+    overflow-wrap : break-word;
+}
+
+div.sphinxsidebar ul {
+    list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox form.search {
+    overflow: hidden;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+    float: left;
+    width: 80%;
+    padding: 0.25em;
+    box-sizing: border-box;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+    float: left;
+    width: 20%;
+    border-left: none;
+    padding: 0.25em;
+    box-sizing: border-box;
+}
+
+
+img {
+    border: 0;
+    max-width: 100%;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+    width: 90%;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 1.3em;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+    width: 100%;
+}
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable ul {
+    margin-top: 0;
+    margin-bottom: 0;
+    list-style-type: none;
+}
+
+table.indextable > tbody > tr > td > ul {
+    padding-left: 0em;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+div.modindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+/* -- domain module index --------------------------------------------------- */
+
+table.modindextable td {
+    padding: 2px;
+    border-collapse: collapse;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+div.body {
+    min-width: 450px;
+    max-width: 800px;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+    -moz-hyphens: auto;
+    -ms-hyphens: auto;
+    -webkit-hyphens: auto;
+    hyphens: auto;
+}
+
+a.headerlink {
+    visibility: hidden;
+}
+
+a.brackets:before,
+span.brackets > a:before{
+    content: "[";
+}
+
+a.brackets:after,
+span.brackets > a:after {
+    content: "]";
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink,
+caption:hover > a.headerlink,
+p.caption:hover > a.headerlink,
+div.code-block-caption:hover > a.headerlink {
+    visibility: visible;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+img.align-left, figure.align-left, .figure.align-left, object.align-left {
+    clear: left;
+    float: left;
+    margin-right: 1em;
+}
+
+img.align-right, figure.align-right, .figure.align-right, object.align-right {
+    clear: right;
+    float: right;
+    margin-left: 1em;
+}
+
+img.align-center, figure.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+img.align-default, figure.align-default, .figure.align-default {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+    text-align: left;
+}
+
+.align-center {
+    text-align: center;
+}
+
+.align-default {
+    text-align: center;
+}
+
+.align-right {
+    text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar,
+aside.sidebar {
+    margin: 0 0 0.5em 1em;
+    border: 1px solid #ddb;
+    padding: 7px;
+    background-color: #ffe;
+    width: 40%;
+    float: right;
+    clear: right;
+    overflow-x: auto;
+}
+
+p.sidebar-title {
+    font-weight: bold;
+}
+
+div.admonition, div.topic, blockquote {
+    clear: left;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+    border: 1px solid #ccc;
+    padding: 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 1.1em;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+/* -- content of sidebars/topics/admonitions -------------------------------- */
+
+div.sidebar > :last-child,
+aside.sidebar > :last-child,
+div.topic > :last-child,
+div.admonition > :last-child {
+    margin-bottom: 0;
+}
+
+div.sidebar::after,
+aside.sidebar::after,
+div.topic::after,
+div.admonition::after,
+blockquote::after {
+    display: block;
+    content: '';
+    clear: both;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    border: 0;
+    border-collapse: collapse;
+}
+
+table.align-center {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table.align-default {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table caption span.caption-number {
+    font-style: italic;
+}
+
+table caption span.caption-text {
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 5px;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+table.citation {
+    border-left: solid 1px gray;
+    margin-left: 1px;
+}
+
+table.citation td {
+    border-bottom: none;
+}
+
+th > :first-child,
+td > :first-child {
+    margin-top: 0px;
+}
+
+th > :last-child,
+td > :last-child {
+    margin-bottom: 0px;
+}
+
+/* -- figures --------------------------------------------------------------- */
+
+div.figure, figure {
+    margin: 0.5em;
+    padding: 0.5em;
+}
+
+div.figure p.caption, figcaption {
+    padding: 0.3em;
+}
+
+div.figure p.caption span.caption-number,
+figcaption span.caption-number {
+    font-style: italic;
+}
+
+div.figure p.caption span.caption-text,
+figcaption span.caption-text {
+}
+
+/* -- field list styles ----------------------------------------------------- */
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+.field-name {
+    -moz-hyphens: manual;
+    -ms-hyphens: manual;
+    -webkit-hyphens: manual;
+    hyphens: manual;
+}
+
+/* -- hlist styles ---------------------------------------------------------- */
+
+table.hlist {
+    margin: 1em 0;
+}
+
+table.hlist td {
+    vertical-align: top;
+}
+
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+    list-style: decimal;
+}
+
+ol.loweralpha {
+    list-style: lower-alpha;
+}
+
+ol.upperalpha {
+    list-style: upper-alpha;
+}
+
+ol.lowerroman {
+    list-style: lower-roman;
+}
+
+ol.upperroman {
+    list-style: upper-roman;
+}
+
+:not(li) > ol > li:first-child > :first-child,
+:not(li) > ul > li:first-child > :first-child {
+    margin-top: 0px;
+}
+
+:not(li) > ol > li:last-child > :last-child,
+:not(li) > ul > li:last-child > :last-child {
+    margin-bottom: 0px;
+}
+
+ol.simple ol p,
+ol.simple ul p,
+ul.simple ol p,
+ul.simple ul p {
+    margin-top: 0;
+}
+
+ol.simple > li:not(:first-child) > p,
+ul.simple > li:not(:first-child) > p {
+    margin-top: 0;
+}
+
+ol.simple p,
+ul.simple p {
+    margin-bottom: 0;
+}
+
+dl.footnote > dt,
+dl.citation > dt {
+    float: left;
+    margin-right: 0.5em;
+}
+
+dl.footnote > dd,
+dl.citation > dd {
+    margin-bottom: 0em;
+}
+
+dl.footnote > dd:after,
+dl.citation > dd:after {
+    content: "";
+    clear: both;
+}
+
+dl.field-list {
+    display: grid;
+    grid-template-columns: fit-content(30%) auto;
+}
+
+dl.field-list > dt {
+    font-weight: bold;
+    word-break: break-word;
+    padding-left: 0.5em;
+    padding-right: 5px;
+}
+
+dl.field-list > dt:after {
+    content: ":";
+}
+
+dl.field-list > dd {
+    padding-left: 0.5em;
+    margin-top: 0em;
+    margin-left: 0em;
+    margin-bottom: 0em;
+}
+
+dl {
+    margin-bottom: 15px;
+}
+
+dd > :first-child {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+dl > dd:last-child,
+dl > dd:last-child > :last-child {
+    margin-bottom: 0;
+}
+
+dt:target, span.highlighted {
+    background-color: #fbe54e;
+}
+
+rect.highlighted {
+    fill: #fbe54e;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+.optional {
+    font-size: 1.3em;
+}
+
+.sig-paren {
+    font-size: larger;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+.footnote:target  {
+    background-color: #ffa;
+}
+
+.line-block {
+    display: block;
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+.line-block .line-block {
+    margin-top: 0;
+    margin-bottom: 0;
+    margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+    font-family: sans-serif;
+}
+
+.accelerator {
+    text-decoration: underline;
+}
+
+.classifier {
+    font-style: oblique;
+}
+
+.classifier:before {
+    font-style: normal;
+    margin: 0.5em;
+    content: ":";
+}
+
+abbr, acronym {
+    border-bottom: dotted 1px;
+    cursor: help;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+    overflow: auto;
+    overflow-y: hidden;  /* fixes display issues on Chrome browsers */
+}
+
+pre, div[class*="highlight-"] {
+    clear: both;
+}
+
+span.pre {
+    -moz-hyphens: none;
+    -ms-hyphens: none;
+    -webkit-hyphens: none;
+    hyphens: none;
+}
+
+div[class*="highlight-"] {
+    margin: 1em 0;
+}
+
+td.linenos pre {
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    display: block;
+}
+
+table.highlighttable tbody {
+    display: block;
+}
+
+table.highlighttable tr {
+    display: flex;
+}
+
+table.highlighttable td {
+    margin: 0;
+    padding: 0;
+}
+
+table.highlighttable td.linenos {
+    padding-right: 0.5em;
+}
+
+table.highlighttable td.code {
+    flex: 1;
+    overflow: hidden;
+}
+
+.highlight .hll {
+    display: block;
+}
+
+div.highlight pre,
+table.highlighttable pre {
+    margin: 0;
+}
+
+div.code-block-caption + div {
+    margin-top: 0;
+}
+
+div.code-block-caption {
+    margin-top: 1em;
+    padding: 2px 5px;
+    font-size: small;
+}
+
+div.code-block-caption code {
+    background-color: transparent;
+}
+
+table.highlighttable td.linenos,
+span.linenos,
+div.doctest > div.highlight span.gp {  /* gp: Generic.Prompt */
+    user-select: none;
+}
+
+div.code-block-caption span.caption-number {
+    padding: 0.1em 0.3em;
+    font-style: italic;
+}
+
+div.code-block-caption span.caption-text {
+}
+
+div.literal-block-wrapper {
+    margin: 1em 0;
+}
+
+code.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+code.descclassname {
+    background-color: transparent;
+}
+
+code.xref, a code {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+    background-color: transparent;
+}
+
+.viewcode-link {
+    float: right;
+}
+
+.viewcode-back {
+    float: right;
+    font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+    margin: -1px -10px;
+    padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+    vertical-align: middle;
+}
+
+div.body div.math p {
+    text-align: center;
+}
+
+span.eqno {
+    float: right;
+}
+
+span.eqno a.headerlink {
+    position: absolute;
+    z-index: 1;
+}
+
+div.math:hover a.headerlink {
+    visibility: visible;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+    div.document,
+    div.documentwrapper,
+    div.bodywrapper {
+        margin: 0 !important;
+        width: 100%;
+    }
+
+    div.sphinxsidebar,
+    div.related,
+    div.footer,
+    #top-link {
+        display: none;
+    }
+}

+ 321 - 0
docs/_build/html/_static/doctools.js

@@ -0,0 +1,321 @@
+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Sphinx JavaScript utilities for all documentation.
+ *
+ * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+/**
+ * make the code below compatible with browsers without
+ * an installed firebug like debugger
+if (!window.console || !console.firebug) {
+  var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
+    "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
+    "profile", "profileEnd"];
+  window.console = {};
+  for (var i = 0; i < names.length; ++i)
+    window.console[names[i]] = function() {};
+}
+ */
+
+/**
+ * small helper function to urldecode strings
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
+ */
+jQuery.urldecode = function(x) {
+  if (!x) {
+    return x
+  }
+  return decodeURIComponent(x.replace(/\+/g, ' '));
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+  if (typeof s === 'undefined')
+    s = document.location.search;
+  var parts = s.substr(s.indexOf('?') + 1).split('&');
+  var result = {};
+  for (var i = 0; i < parts.length; i++) {
+    var tmp = parts[i].split('=', 2);
+    var key = jQuery.urldecode(tmp[0]);
+    var value = jQuery.urldecode(tmp[1]);
+    if (key in result)
+      result[key].push(value);
+    else
+      result[key] = [value];
+  }
+  return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+  function highlight(node, addItems) {
+    if (node.nodeType === 3) {
+      var val = node.nodeValue;
+      var pos = val.toLowerCase().indexOf(text);
+      if (pos >= 0 &&
+          !jQuery(node.parentNode).hasClass(className) &&
+          !jQuery(node.parentNode).hasClass("nohighlight")) {
+        var span;
+        var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+        if (isInSVG) {
+          span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+        } else {
+          span = document.createElement("span");
+          span.className = className;
+        }
+        span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+        node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+          document.createTextNode(val.substr(pos + text.length)),
+          node.nextSibling));
+        node.nodeValue = val.substr(0, pos);
+        if (isInSVG) {
+          var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+          var bbox = node.parentElement.getBBox();
+          rect.x.baseVal.value = bbox.x;
+          rect.y.baseVal.value = bbox.y;
+          rect.width.baseVal.value = bbox.width;
+          rect.height.baseVal.value = bbox.height;
+          rect.setAttribute('class', className);
+          addItems.push({
+              "parent": node.parentNode,
+              "target": rect});
+        }
+      }
+    }
+    else if (!jQuery(node).is("button, select, textarea")) {
+      jQuery.each(node.childNodes, function() {
+        highlight(this, addItems);
+      });
+    }
+  }
+  var addItems = [];
+  var result = this.each(function() {
+    highlight(this, addItems);
+  });
+  for (var i = 0; i < addItems.length; ++i) {
+    jQuery(addItems[i].parent).before(addItems[i].target);
+  }
+  return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+  jQuery.uaMatch = function(ua) {
+    ua = ua.toLowerCase();
+
+    var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+      /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+      /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+      /(msie) ([\w.]+)/.exec(ua) ||
+      ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+      [];
+
+    return {
+      browser: match[ 1 ] || "",
+      version: match[ 2 ] || "0"
+    };
+  };
+  jQuery.browser = {};
+  jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+var Documentation = {
+
+  init : function() {
+    this.fixFirefoxAnchorBug();
+    this.highlightSearchWords();
+    this.initIndexTable();
+    if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
+      this.initOnKeyListeners();
+    }
+  },
+
+  /**
+   * i18n support
+   */
+  TRANSLATIONS : {},
+  PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
+  LOCALE : 'unknown',
+
+  // gettext and ngettext don't access this so that the functions
+  // can safely bound to a different name (_ = Documentation.gettext)
+  gettext : function(string) {
+    var translated = Documentation.TRANSLATIONS[string];
+    if (typeof translated === 'undefined')
+      return string;
+    return (typeof translated === 'string') ? translated : translated[0];
+  },
+
+  ngettext : function(singular, plural, n) {
+    var translated = Documentation.TRANSLATIONS[singular];
+    if (typeof translated === 'undefined')
+      return (n == 1) ? singular : plural;
+    return translated[Documentation.PLURALEXPR(n)];
+  },
+
+  addTranslations : function(catalog) {
+    for (var key in catalog.messages)
+      this.TRANSLATIONS[key] = catalog.messages[key];
+    this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
+    this.LOCALE = catalog.locale;
+  },
+
+  /**
+   * add context elements like header anchor links
+   */
+  addContextElements : function() {
+    $('div[id] > :header:first').each(function() {
+      $('<a class="headerlink">\u00B6</a>').
+      attr('href', '#' + this.id).
+      attr('title', _('Permalink to this headline')).
+      appendTo(this);
+    });
+    $('dt[id]').each(function() {
+      $('<a class="headerlink">\u00B6</a>').
+      attr('href', '#' + this.id).
+      attr('title', _('Permalink to this definition')).
+      appendTo(this);
+    });
+  },
+
+  /**
+   * workaround a firefox stupidity
+   * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
+   */
+  fixFirefoxAnchorBug : function() {
+    if (document.location.hash && $.browser.mozilla)
+      window.setTimeout(function() {
+        document.location.href += '';
+      }, 10);
+  },
+
+  /**
+   * highlight the search words provided in the url in the text
+   */
+  highlightSearchWords : function() {
+    var params = $.getQueryParameters();
+    var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
+    if (terms.length) {
+      var body = $('div.body');
+      if (!body.length) {
+        body = $('body');
+      }
+      window.setTimeout(function() {
+        $.each(terms, function() {
+          body.highlightText(this.toLowerCase(), 'highlighted');
+        });
+      }, 10);
+      $('<p class="highlight-link"><a href="javascript:Documentation.' +
+        'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
+          .appendTo($('#searchbox'));
+    }
+  },
+
+  /**
+   * init the domain index toggle buttons
+   */
+  initIndexTable : function() {
+    var togglers = $('img.toggler').click(function() {
+      var src = $(this).attr('src');
+      var idnum = $(this).attr('id').substr(7);
+      $('tr.cg-' + idnum).toggle();
+      if (src.substr(-9) === 'minus.png')
+        $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
+      else
+        $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
+    }).css('display', '');
+    if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
+        togglers.click();
+    }
+  },
+
+  /**
+   * helper function to hide the search marks again
+   */
+  hideSearchWords : function() {
+    $('#searchbox .highlight-link').fadeOut(300);
+    $('span.highlighted').removeClass('highlighted');
+  },
+
+  /**
+   * make the url absolute
+   */
+  makeURL : function(relativeURL) {
+    return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
+  },
+
+  /**
+   * get the current relative url
+   */
+  getCurrentURL : function() {
+    var path = document.location.pathname;
+    var parts = path.split(/\//);
+    $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
+      if (this === '..')
+        parts.pop();
+    });
+    var url = parts.join('/');
+    return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
+  },
+
+  initOnKeyListeners: function() {
+    $(document).keydown(function(event) {
+      var activeElementType = document.activeElement.tagName;
+      // don't navigate when in search box, textarea, dropdown or button
+      if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
+          && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
+          && !event.shiftKey) {
+        switch (event.keyCode) {
+          case 37: // left
+            var prevHref = $('link[rel="prev"]').prop('href');
+            if (prevHref) {
+              window.location.href = prevHref;
+              return false;
+            }
+          case 39: // right
+            var nextHref = $('link[rel="next"]').prop('href');
+            if (nextHref) {
+              window.location.href = nextHref;
+              return false;
+            }
+        }
+      }
+    });
+  }
+};
+
+// quick alias for translations
+_ = Documentation.gettext;
+
+$(document).ready(function() {
+  Documentation.init();
+});

+ 12 - 0
docs/_build/html/_static/documentation_options.js

@@ -0,0 +1,12 @@
+var DOCUMENTATION_OPTIONS = {
+    URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
+    VERSION: '0.9.0',
+    LANGUAGE: 'None',
+    COLLAPSE_INDEX: false,
+    BUILDER: 'html',
+    FILE_SUFFIX: '.html',
+    LINK_SUFFIX: '.html',
+    HAS_SOURCE: true,
+    SOURCELINK_SUFFIX: '.txt',
+    NAVIGATION_WITH_KEYS: false
+};

BIN
docs/_build/html/_static/file.png


File diff suppressed because it is too large
+ 3 - 0
docs/_build/html/_static/fonts/font-awesome.css


+ 13 - 0
docs/_build/html/_static/fonts/material-icons.css

@@ -0,0 +1,13 @@
+/*!
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at:
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, SOFTWARE
+ * DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
+ * SEE THE LICENSE FOR THE SPECIFIC LANGUAGE GOVERNING PERMISSIONS AND
+ * LIMITATIONS UNDER THE LICENSE.
+ */@font-face{font-display:swap;font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url("specimen/MaterialIcons-Regular.woff2") format("woff2"),url("specimen/MaterialIcons-Regular.woff") format("woff"),url("specimen/MaterialIcons-Regular.ttf") format("truetype")}

BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.ttf


BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.woff


BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.woff2


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.ttf


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff2


BIN
docs/_build/html/_static/images/favicon.png


+ 1 - 0
docs/_build/html/_static/images/icons/bitbucket.1b09e088.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="352" height="448" viewBox="0 0 352 448" id="__bitbucket"><path fill="currentColor" d="M203.75 214.75q2 15.75-12.625 25.25t-27.875 1.5q-9.75-4.25-13.375-14.5t-.125-20.5 13-14.5q9-4.5 18.125-3t16 8.875 6.875 16.875zm27.75-5.25q-3.5-26.75-28.25-41T154 165.25q-15.75 7-25.125 22.125t-8.625 32.375q1 22.75 19.375 38.75t41.375 14q22.75-2 38-21t12.5-42zM291.25 74q-5-6.75-14-11.125t-14.5-5.5T245 54.25q-72.75-11.75-141.5.5-10.75 1.75-16.5 3t-13.75 5.5T60.75 74q7.5 7 19 11.375t18.375 5.5T120 93.75Q177 101 232 94q15.75-2 22.375-3t18.125-5.375T291.25 74zm14.25 258.75q-2 6.5-3.875 19.125t-3.5 21-7.125 17.5-14.5 14.125q-21.5 12-47.375 17.875t-50.5 5.5-50.375-4.625q-11.5-2-20.375-4.5T88.75 412 70.5 401.125t-13-15.375q-6.25-24-14.25-73l1.5-4 4.5-2.25q55.75 37 126.625 37t126.875-37q5.25 1.5 6 5.75t-1.25 11.25-2 9.25zM350.75 92.5q-6.5 41.75-27.75 163.75-1.25 7.5-6.75 14t-10.875 10T291.75 288q-63 31.5-152.5 22-62-6.75-98.5-34.75-3.75-3-6.375-6.625t-4.25-8.75-2.25-8.5-1.5-9.875T25 232.75q-2.25-12.5-6.625-37.5t-7-40.375T5.5 118 0 78.5Q.75 72 4.375 66.375T12.25 57t11.25-7.5T35 43.875t12-4.625q31.25-11.5 78.25-16 94.75-9.25 169 12.5Q333 47.25 348 66.25q4 5 4.125 12.75t-1.375 13.5z"/></svg>

+ 1 - 0
docs/_build/html/_static/images/icons/bitbucket.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="352" height="448" viewBox="0 0 352 448" id="__bitbucket"><path fill="currentColor" d="M203.75 214.75q2 15.75-12.625 25.25t-27.875 1.5q-9.75-4.25-13.375-14.5t-.125-20.5 13-14.5q9-4.5 18.125-3t16 8.875 6.875 16.875zm27.75-5.25q-3.5-26.75-28.25-41T154 165.25q-15.75 7-25.125 22.125t-8.625 32.375q1 22.75 19.375 38.75t41.375 14q22.75-2 38-21t12.5-42zM291.25 74q-5-6.75-14-11.125t-14.5-5.5T245 54.25q-72.75-11.75-141.5.5-10.75 1.75-16.5 3t-13.75 5.5T60.75 74q7.5 7 19 11.375t18.375 5.5T120 93.75Q177 101 232 94q15.75-2 22.375-3t18.125-5.375T291.25 74zm14.25 258.75q-2 6.5-3.875 19.125t-3.5 21-7.125 17.5-14.5 14.125q-21.5 12-47.375 17.875t-50.5 5.5-50.375-4.625q-11.5-2-20.375-4.5T88.75 412 70.5 401.125t-13-15.375q-6.25-24-14.25-73l1.5-4 4.5-2.25q55.75 37 126.625 37t126.875-37q5.25 1.5 6 5.75t-1.25 11.25-2 9.25zM350.75 92.5q-6.5 41.75-27.75 163.75-1.25 7.5-6.75 14t-10.875 10T291.75 288q-63 31.5-152.5 22-62-6.75-98.5-34.75-3.75-3-6.375-6.625t-4.25-8.75-2.25-8.5-1.5-9.875T25 232.75q-2.25-12.5-6.625-37.5t-7-40.375T5.5 118 0 78.5Q.75 72 4.375 66.375T12.25 57t11.25-7.5T35 43.875t12-4.625q31.25-11.5 78.25-16 94.75-9.25 169 12.5Q333 47.25 348 66.25q4 5 4.125 12.75t-1.375 13.5z"/></svg>

+ 1 - 0
docs/_build/html/_static/images/icons/github.f0b8504a.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="416" height="448" viewBox="0 0 416 448" id="__github"><path fill="currentColor" d="M160 304q0 10-3.125 20.5t-10.75 19T128 352t-18.125-8.5-10.75-19T96 304t3.125-20.5 10.75-19T128 256t18.125 8.5 10.75 19T160 304zm160 0q0 10-3.125 20.5t-10.75 19T288 352t-18.125-8.5-10.75-19T256 304t3.125-20.5 10.75-19T288 256t18.125 8.5 10.75 19T320 304zm40 0q0-30-17.25-51T296 232q-10.25 0-48.75 5.25Q229.5 240 208 240t-39.25-2.75Q130.75 232 120 232q-29.5 0-46.75 21T56 304q0 22 8 38.375t20.25 25.75 30.5 15 35 7.375 37.25 1.75h42q20.5 0 37.25-1.75t35-7.375 30.5-15 20.25-25.75T360 304zm56-44q0 51.75-15.25 82.75-9.5 19.25-26.375 33.25t-35.25 21.5-42.5 11.875-42.875 5.5T212 416q-19.5 0-35.5-.75t-36.875-3.125-38.125-7.5-34.25-12.875T37 371.5t-21.5-28.75Q0 312 0 260q0-59.25 34-99-6.75-20.5-6.75-42.5 0-29 12.75-54.5 27 0 47.5 9.875t47.25 30.875Q171.5 96 212 96q37 0 70 8 26.25-20.5 46.75-30.25T376 64q12.75 25.5 12.75 54.5 0 21.75-6.75 42 34 40 34 99.5z"/></svg>

+ 1 - 0
docs/_build/html/_static/images/icons/github.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="416" height="448" viewBox="0 0 416 448" id="__github"><path fill="currentColor" d="M160 304q0 10-3.125 20.5t-10.75 19T128 352t-18.125-8.5-10.75-19T96 304t3.125-20.5 10.75-19T128 256t18.125 8.5 10.75 19T160 304zm160 0q0 10-3.125 20.5t-10.75 19T288 352t-18.125-8.5-10.75-19T256 304t3.125-20.5 10.75-19T288 256t18.125 8.5 10.75 19T320 304zm40 0q0-30-17.25-51T296 232q-10.25 0-48.75 5.25Q229.5 240 208 240t-39.25-2.75Q130.75 232 120 232q-29.5 0-46.75 21T56 304q0 22 8 38.375t20.25 25.75 30.5 15 35 7.375 37.25 1.75h42q20.5 0 37.25-1.75t35-7.375 30.5-15 20.25-25.75T360 304zm56-44q0 51.75-15.25 82.75-9.5 19.25-26.375 33.25t-35.25 21.5-42.5 11.875-42.875 5.5T212 416q-19.5 0-35.5-.75t-36.875-3.125-38.125-7.5-34.25-12.875T37 371.5t-21.5-28.75Q0 312 0 260q0-59.25 34-99-6.75-20.5-6.75-42.5 0-29 12.75-54.5 27 0 47.5 9.875t47.25 30.875Q171.5 96 212 96q37 0 70 8 26.25-20.5 46.75-30.25T376 64q12.75 25.5 12.75 54.5 0 21.75-6.75 42 34 40 34 99.5z"/></svg>

+ 1 - 0
docs/_build/html/_static/images/icons/gitlab.6dd19c00.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500" id="__gitlab"><path fill="currentColor" d="M93.667 473.347l90.684-279.097H2.983l90.684 279.097z" transform="translate(156.198 1.16)"/><path fill="currentColor" d="M221.333 473.345L130.649 194.25H3.557l217.776 279.095z" transform="translate(28.531 1.16)" opacity=".7"/><path fill="currentColor" d="M32 195.155L4.441 279.97a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29L32 195.155z" transform="translate(.089 .256)" opacity=".5"/><path fill="currentColor" d="M2.667-84.844h127.092L75.14-252.942c-2.811-8.649-15.047-8.649-17.856 0L2.667-84.844z" transform="translate(29.422 280.256)"/><path fill="currentColor" d="M2.667 473.345L93.351 194.25h127.092L2.667 473.345z" transform="translate(247.198 1.16)" opacity=".7"/><path fill="currentColor" d="M221.334 195.155l27.559 84.815a18.772 18.772 0 0 1-6.821 20.99L3.557 474.25l217.777-279.095z" transform="translate(246.307 .256)" opacity=".5"/><path fill="currentColor" d="M130.667-84.844H3.575l54.618-168.098c2.811-8.649 15.047-8.649 17.856 0l54.618 168.098z" transform="translate(336.974 280.256)"/></svg>

+ 1 - 0
docs/_build/html/_static/images/icons/gitlab.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500" id="__gitlab"><path fill="currentColor" d="M93.667 473.347l90.684-279.097H2.983l90.684 279.097z" transform="translate(156.198 1.16)"/><path fill="currentColor" d="M221.333 473.345L130.649 194.25H3.557l217.776 279.095z" transform="translate(28.531 1.16)" opacity=".7"/><path fill="currentColor" d="M32 195.155L4.441 279.97a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29L32 195.155z" transform="translate(.089 .256)" opacity=".5"/><path fill="currentColor" d="M2.667-84.844h127.092L75.14-252.942c-2.811-8.649-15.047-8.649-17.856 0L2.667-84.844z" transform="translate(29.422 280.256)"/><path fill="currentColor" d="M2.667 473.345L93.351 194.25h127.092L2.667 473.345z" transform="translate(247.198 1.16)" opacity=".7"/><path fill="currentColor" d="M221.334 195.155l27.559 84.815a18.772 18.772 0 0 1-6.821 20.99L3.557 474.25l217.777-279.095z" transform="translate(246.307 .256)" opacity=".5"/><path fill="currentColor" d="M130.667-84.844H3.575l54.618-168.098c2.811-8.649 15.047-8.649 17.856 0l54.618 168.098z" transform="translate(336.974 280.256)"/></svg>

+ 2540 - 0
docs/_build/html/_static/javascripts/application.js

@@ -0,0 +1,2540 @@
+! function(e, t) {
+    for (var n in t) e[n] = t[n]
+}(window, function(n) {
+    var r = {};
+
+    function i(e) {
+        if (r[e]) return r[e].exports;
+        var t = r[e] = {
+            i: e,
+            l: !1,
+            exports: {}
+        };
+        return n[e].call(t.exports, t, t.exports, i), t.l = !0, t.exports
+    }
+    return i.m = n, i.c = r, i.d = function(e, t, n) {
+        i.o(e, t) || Object.defineProperty(e, t, {
+            enumerable: !0,
+            get: n
+        })
+    }, i.r = function(e) {
+        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
+            value: "Module"
+        }), Object.defineProperty(e, "__esModule", {
+            value: !0
+        })
+    }, i.t = function(t, e) {
+        if (1 & e && (t = i(t)), 8 & e) return t;
+        if (4 & e && "object" == typeof t && t && t.__esModule) return t;
+        var n = Object.create(null);
+        if (i.r(n), Object.defineProperty(n, "default", {
+                enumerable: !0,
+                value: t
+            }), 2 & e && "string" != typeof t)
+            for (var r in t) i.d(n, r, function(e) {
+                return t[e]
+            }.bind(null, r));
+        return n
+    }, i.n = function(e) {
+        var t = e && e.__esModule ? function() {
+            return e.default
+        } : function() {
+            return e
+        };
+        return i.d(t, "a", t), t
+    }, i.o = function(e, t) {
+        return Object.prototype.hasOwnProperty.call(e, t)
+    }, i.p = "", i(i.s = 13)
+}([function(e, t, n) {
+    "use strict";
+    var r = {
+            Listener: function() {
+                function e(e, t, n) {
+                    var r = this;
+                    this.els_ = Array.prototype.slice.call("string" == typeof e ? document.querySelectorAll(e) : [].concat(e)), this.handler_ = "function" == typeof n ? {
+                        update: n
+                    } : n, this.events_ = [].concat(t), this.update_ = function(e) {
+                        return r.handler_.update(e)
+                    }
+                }
+                var t = e.prototype;
+                return t.listen = function() {
+                    var n = this;
+                    this.els_.forEach(function(t) {
+                        n.events_.forEach(function(e) {
+                            t.addEventListener(e, n.update_, !1)
+                        })
+                    }), "function" == typeof this.handler_.setup && this.handler_.setup()
+                }, t.unlisten = function() {
+                    var n = this;
+                    this.els_.forEach(function(t) {
+                        n.events_.forEach(function(e) {
+                            t.removeEventListener(e, n.update_)
+                        })
+                    }), "function" == typeof this.handler_.reset && this.handler_.reset()
+                }, e
+            }(),
+            MatchMedia: function(e, t) {
+                this.handler_ = function(e) {
+                    e.matches ? t.listen() : t.unlisten()
+                };
+                var n = window.matchMedia(e);
+                n.addListener(this.handler_), this.handler_(n)
+            }
+        },
+        i = {
+            Shadow: function() {
+                function e(e, t) {
+                    var n = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(n instanceof HTMLElement && n.parentNode instanceof HTMLElement)) throw new ReferenceError;
+                    if (this.el_ = n.parentNode, !((n = "string" == typeof t ? document.querySelector(t) : t) instanceof HTMLElement)) throw new ReferenceError;
+                    this.header_ = n, this.height_ = 0, this.active_ = !1
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    for (var e = this.el_; e = e.previousElementSibling;) {
+                        if (!(e instanceof HTMLElement)) throw new ReferenceError;
+                        this.height_ += e.offsetHeight
+                    }
+                    this.update()
+                }, t.update = function(e) {
+                    if (!e || "resize" !== e.type && "orientationchange" !== e.type) {
+                        var t = window.pageYOffset >= this.height_;
+                        t !== this.active_ && (this.header_.dataset.mdState = (this.active_ = t) ? "shadow" : "")
+                    } else this.height_ = 0, this.setup()
+                }, t.reset = function() {
+                    this.header_.dataset.mdState = "", this.height_ = 0, this.active_ = !1
+                }, e
+            }(),
+            Title: function() {
+                function e(e, t) {
+                    var n = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(n instanceof HTMLElement)) throw new ReferenceError;
+                    if (this.el_ = n, !((n = "string" == typeof t ? document.querySelector(t) : t) instanceof HTMLHeadingElement)) throw new ReferenceError;
+                    this.header_ = n, this.active_ = !1
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    var t = this;
+                    Array.prototype.forEach.call(this.el_.children, function(e) {
+                        e.style.width = t.el_.offsetWidth - 20 + "px"
+                    })
+                }, t.update = function(e) {
+                    var t = this,
+                        n = window.pageYOffset >= this.header_.offsetTop;
+                    n !== this.active_ && (this.el_.dataset.mdState = (this.active_ = n) ? "active" : ""), "resize" !== e.type && "orientationchange" !== e.type || Array.prototype.forEach.call(this.el_.children, function(e) {
+                        e.style.width = t.el_.offsetWidth - 20 + "px"
+                    })
+                }, t.reset = function() {
+                    this.el_.dataset.mdState = "", this.el_.style.width = "", this.active_ = !1
+                }, e
+            }()
+        },
+        o = {
+            Blur: function() {
+                function e(e) {
+                    this.els_ = "string" == typeof e ? document.querySelectorAll(e) : e, this.index_ = 0, this.offset_ = window.pageYOffset, this.dir_ = !1, this.anchors_ = [].reduce.call(this.els_, function(e, t) {
+                        var n = decodeURIComponent(t.hash);
+                        return e.concat(document.getElementById(n.substring(1)) || [])
+                    }, [])
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    this.update()
+                }, t.update = function() {
+                    var e = window.pageYOffset,
+                        t = this.offset_ - e < 0;
+                    if (this.dir_ !== t && (this.index_ = this.index_ = t ? 0 : this.els_.length - 1), 0 !== this.anchors_.length) {
+                        if (this.offset_ <= e)
+                            for (var n = this.index_ + 1; n < this.els_.length && this.anchors_[n].offsetTop - 80 <= e; n++) 0 < n && (this.els_[n - 1].dataset.mdState = "blur"), this.index_ = n;
+                        else
+                            for (var r = this.index_; 0 <= r; r--) {
+                                if (!(this.anchors_[r].offsetTop - 80 > e)) {
+                                    this.index_ = r;
+                                    break
+                                }
+                                0 < r && (this.els_[r - 1].dataset.mdState = "")
+                            }
+                        this.offset_ = e, this.dir_ = t
+                    }
+                }, t.reset = function() {
+                    Array.prototype.forEach.call(this.els_, function(e) {
+                        e.dataset.mdState = ""
+                    }), this.index_ = 0, this.offset_ = window.pageYOffset
+                }, e
+            }(),
+            Collapse: function() {
+                function e(e) {
+                    var t = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                    this.el_ = t
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    var e = this.el_.getBoundingClientRect().height;
+                    this.el_.style.display = e ? "block" : "none", this.el_.style.overflow = e ? "visible" : "hidden"
+                }, t.update = function() {
+                    var e = this,
+                        t = this.el_.getBoundingClientRect().height;
+                    this.el_.style.display = "block", this.el_.style.overflow = "";
+                    var r = this.el_.previousElementSibling.previousElementSibling.checked;
+                    if (r) this.el_.style.maxHeight = t + "px", requestAnimationFrame(function() {
+                        e.el_.setAttribute("data-md-state", "animate"), e.el_.style.maxHeight = "0px"
+                    });
+                    else {
+                        this.el_.setAttribute("data-md-state", "expand"), this.el_.style.maxHeight = "";
+                        var n = this.el_.getBoundingClientRect().height;
+                        this.el_.removeAttribute("data-md-state"), this.el_.style.maxHeight = "0px", requestAnimationFrame(function() {
+                            e.el_.setAttribute("data-md-state", "animate"), e.el_.style.maxHeight = n + "px"
+                        })
+                    }
+                    this.el_.addEventListener("transitionend", function e(t) {
+                        var n = t.target;
+                        if (!(n instanceof HTMLElement)) throw new ReferenceError;
+                        n.removeAttribute("data-md-state"), n.style.maxHeight = "", n.style.display = r ? "none" : "block", n.style.overflow = r ? "hidden" : "visible", n.removeEventListener("transitionend", e)
+                    }, !1)
+                }, t.reset = function() {
+                    this.el_.dataset.mdState = "", this.el_.style.maxHeight = "", this.el_.style.display = "", this.el_.style.overflow = ""
+                }, e
+            }(),
+            Scrolling: function() {
+                function e(e) {
+                    var t = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                    this.el_ = t
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    this.el_.children[this.el_.children.length - 1].style.webkitOverflowScrolling = "touch";
+                    var e = this.el_.querySelectorAll("[data-md-toggle]");
+                    Array.prototype.forEach.call(e, function(e) {
+                        if (!(e instanceof HTMLInputElement)) throw new ReferenceError;
+                        if (e.checked) {
+                            var t = e.nextElementSibling;
+                            if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                            for (;
+                                "NAV" !== t.tagName && t.nextElementSibling;) t = t.nextElementSibling;
+                            if (!(e.parentNode instanceof HTMLElement && e.parentNode.parentNode instanceof HTMLElement)) throw new ReferenceError;
+                            var n = e.parentNode.parentNode,
+                                r = t.children[t.children.length - 1];
+                            n.style.webkitOverflowScrolling = "", r.style.webkitOverflowScrolling = "touch"
+                        }
+                    })
+                }, t.update = function(e) {
+                    var t = e.target;
+                    if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                    var n = t.nextElementSibling;
+                    if (!(n instanceof HTMLElement)) throw new ReferenceError;
+                    for (;
+                        "NAV" !== n.tagName && n.nextElementSibling;) n = n.nextElementSibling;
+                    if (!(t.parentNode instanceof HTMLElement && t.parentNode.parentNode instanceof HTMLElement)) throw new ReferenceError;
+                    var r = t.parentNode.parentNode,
+                        i = n.children[n.children.length - 1];
+                    if (r.style.webkitOverflowScrolling = "", i.style.webkitOverflowScrolling = "", !t.checked) {
+                        n.addEventListener("transitionend", function e() {
+                            n instanceof HTMLElement && (r.style.webkitOverflowScrolling = "touch", n.removeEventListener("transitionend", e))
+                        }, !1)
+                    }
+                    if (t.checked) {
+                        n.addEventListener("transitionend", function e() {
+                            n instanceof HTMLElement && (i.style.webkitOverflowScrolling = "touch", n.removeEventListener("transitionend", e))
+                        }, !1)
+                    }
+                }, t.reset = function() {
+                    this.el_.children[1].style.webkitOverflowScrolling = "";
+                    var e = this.el_.querySelectorAll("[data-md-toggle]");
+                    Array.prototype.forEach.call(e, function(e) {
+                        if (!(e instanceof HTMLInputElement)) throw new ReferenceError;
+                        if (e.checked) {
+                            var t = e.nextElementSibling;
+                            if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                            for (;
+                                "NAV" !== t.tagName && t.nextElementSibling;) t = t.nextElementSibling;
+                            if (!(e.parentNode instanceof HTMLElement && e.parentNode.parentNode instanceof HTMLElement)) throw new ReferenceError;
+                            var n = e.parentNode.parentNode,
+                                r = t.children[t.children.length - 1];
+                            n.style.webkitOverflowScrolling = "", r.style.webkitOverflowScrolling = ""
+                        }
+                    })
+                }, e
+            }()
+        },
+        a = {
+            Lock: function() {
+                function e(e) {
+                    var t = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(t instanceof HTMLInputElement)) throw new ReferenceError;
+                    if (this.el_ = t, !document.body) throw new ReferenceError;
+                    this.lock_ = document.body
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    this.update()
+                }, t.update = function() {
+                    var e = this;
+                    this.el_.checked ? (this.offset_ = window.pageYOffset, setTimeout(function() {
+                        window.scrollTo(0, 0), e.el_.checked && (e.lock_.dataset.mdState = "lock")
+                    }, 400)) : (this.lock_.dataset.mdState = "", setTimeout(function() {
+                        void 0 !== e.offset_ && window.scrollTo(0, e.offset_)
+                    }, 100))
+                }, t.reset = function() {
+                    "lock" === this.lock_.dataset.mdState && window.scrollTo(0, this.offset_), this.lock_.dataset.mdState = ""
+                }, e
+            }(),
+            Result: n(9).a
+        },
+        s = {
+            Position: function() {
+                function e(e, t) {
+                    var n = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(n instanceof HTMLElement && n.parentNode instanceof HTMLElement)) throw new ReferenceError;
+                    if (this.el_ = n, this.parent_ = n.parentNode, !((n = "string" == typeof t ? document.querySelector(t) : t) instanceof HTMLElement)) throw new ReferenceError;
+                    this.header_ = n, this.height_ = 0, this.pad_ = "fixed" === window.getComputedStyle(this.header_).position
+                }
+                var t = e.prototype;
+                return t.setup = function() {
+                    var e = Array.prototype.reduce.call(this.parent_.children, function(e, t) {
+                        return Math.max(e, t.offsetTop)
+                    }, 0);
+                    this.offset_ = e - (this.pad_ ? this.header_.offsetHeight : 0), this.update()
+                }, t.update = function(e) {
+                    var t = window.pageYOffset,
+                        n = window.innerHeight;
+                    e && "resize" === e.type && this.setup();
+                    var r = this.pad_ ? this.header_.offsetHeight : 0,
+                        i = this.parent_.offsetTop + this.parent_.offsetHeight,
+                        o = n - r - Math.max(0, this.offset_ - t) - Math.max(0, t + n - i);
+                    o !== this.height_ && (this.el_.style.height = (this.height_ = o) + "px"), t >= this.offset_ ? "lock" !== this.el_.dataset.mdState && (this.el_.dataset.mdState = "lock") : "lock" === this.el_.dataset.mdState && (this.el_.dataset.mdState = "")
+                }, t.reset = function() {
+                    this.el_.dataset.mdState = "", this.el_.style.height = "", this.height_ = 0
+                }, e
+            }()
+        },
+        c = n(6),
+        l = n.n(c);
+    var u = {
+            Adapter: {
+                GitHub: function(o) {
+                    var e, t;
+
+                    function n(e) {
+                        var t;
+                        t = o.call(this, e) || this;
+                        var n = /^.+github\.com\/([^/]+)\/?([^/]+)?.*$/.exec(t.base_);
+                        if (n && 3 === n.length) {
+                            var r = n[1],
+                                i = n[2];
+                            t.base_ = "https://api.github.com/users/" + r + "/repos", t.name_ = i
+                        }
+                        return t
+                    }
+                    return t = o, (e = n).prototype = Object.create(t.prototype), (e.prototype.constructor = e).__proto__ = t, n.prototype.fetch_ = function() {
+                        var i = this;
+                        return function n(r) {
+                            return void 0 === r && (r = 0), fetch(i.base_ + "?per_page=30&page=" + r).then(function(e) {
+                                return e.json()
+                            }).then(function(e) {
+                                if (!(e instanceof Array)) throw new TypeError;
+                                if (i.name_) {
+                                    var t = e.find(function(e) {
+                                        return e.name === i.name_
+                                    });
+                                    return t || 30 !== e.length ? t ? [i.format_(t.stargazers_count) + " Stars", i.format_(t.forks_count) + " Forks"] : [] : n(r + 1)
+                                }
+                                return [e.length + " Repositories"]
+                            })
+                        }()
+                    }, n
+                }(function() {
+                    function e(e) {
+                        var t = "string" == typeof e ? document.querySelector(e) : e;
+                        if (!(t instanceof HTMLAnchorElement)) throw new ReferenceError;
+                        this.el_ = t, this.base_ = this.el_.href, this.salt_ = this.hash_(this.base_)
+                    }
+                    var t = e.prototype;
+                    return t.fetch = function() {
+                        var n = this;
+                        return new Promise(function(t) {
+                            var e = l.a.getJSON(n.salt_ + ".cache-source");
+                            void 0 !== e ? t(e) : n.fetch_().then(function(e) {
+                                l.a.set(n.salt_ + ".cache-source", e, {
+                                    expires: 1 / 96
+                                }), t(e)
+                            })
+                        })
+                    }, t.fetch_ = function() {
+                        throw new Error("fetch_(): Not implemented")
+                    }, t.format_ = function(e) {
+                        return 1e4 < e ? (e / 1e3).toFixed(0) + "k" : 1e3 < e ? (e / 1e3).toFixed(1) + "k" : "" + e
+                    }, t.hash_ = function(e) {
+                        var t = 0;
+                        if (0 === e.length) return t;
+                        for (var n = 0, r = e.length; n < r; n++) t = (t << 5) - t + e.charCodeAt(n), t |= 0;
+                        return t
+                    }, e
+                }())
+            },
+            Repository: n(10).a
+        },
+        f = {
+            Toggle: function() {
+                function e(e) {
+                    var t = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(t instanceof Node)) throw new ReferenceError;
+                    this.el_ = t;
+                    var n = document.querySelector("[data-md-component=header]");
+                    this.height_ = n.offsetHeight, this.active_ = !1
+                }
+                var t = e.prototype;
+                return t.update = function() {
+                    var e = window.pageYOffset >= this.el_.children[0].offsetTop + (5 - this.height_);
+                    e !== this.active_ && (this.el_.dataset.mdState = (this.active_ = e) ? "hidden" : "")
+                }, t.reset = function() {
+                    this.el_.dataset.mdState = "", this.active_ = !1
+                }, e
+            }()
+        };
+    t.a = {
+        Event: r,
+        Header: i,
+        Nav: o,
+        Search: a,
+        Sidebar: s,
+        Source: u,
+        Tabs: f
+    }
+}, function(t, e, n) {
+    (function(e) {
+        t.exports = e.lunr = n(24)
+    }).call(this, n(4))
+}, function(e, f, d) {
+    "use strict";
+    (function(t) {
+        var e = d(8),
+            n = setTimeout;
+
+        function r() {}
+
+        function o(e) {
+            if (!(this instanceof o)) throw new TypeError("Promises must be constructed via new");
+            if ("function" != typeof e) throw new TypeError("not a function");
+            this._state = 0, this._handled = !1, this._value = void 0, this._deferreds = [], u(e, this)
+        }
+
+        function i(n, r) {
+            for (; 3 === n._state;) n = n._value;
+            0 !== n._state ? (n._handled = !0, o._immediateFn(function() {
+                var e = 1 === n._state ? r.onFulfilled : r.onRejected;
+                if (null !== e) {
+                    var t;
+                    try {
+                        t = e(n._value)
+                    } catch (e) {
+                        return void s(r.promise, e)
+                    }
+                    a(r.promise, t)
+                } else(1 === n._state ? a : s)(r.promise, n._value)
+            })) : n._deferreds.push(r)
+        }
+
+        function a(t, e) {
+            try {
+                if (e === t) throw new TypeError("A promise cannot be resolved with itself.");
+                if (e && ("object" == typeof e || "function" == typeof e)) {
+                    var n = e.then;
+                    if (e instanceof o) return t._state = 3, t._value = e, void c(t);
+                    if ("function" == typeof n) return void u((r = n, i = e, function() {
+                        r.apply(i, arguments)
+                    }), t)
+                }
+                t._state = 1, t._value = e, c(t)
+            } catch (e) {
+                s(t, e)
+            }
+            var r, i
+        }
+
+        function s(e, t) {
+            e._state = 2, e._value = t, c(e)
+        }
+
+        function c(e) {
+            2 === e._state && 0 === e._deferreds.length && o._immediateFn(function() {
+                e._handled || o._unhandledRejectionFn(e._value)
+            });
+            for (var t = 0, n = e._deferreds.length; t < n; t++) i(e, e._deferreds[t]);
+            e._deferreds = null
+        }
+
+        function l(e, t, n) {
+            this.onFulfilled = "function" == typeof e ? e : null, this.onRejected = "function" == typeof t ? t : null, this.promise = n
+        }
+
+        function u(e, t) {
+            var n = !1;
+            try {
+                e(function(e) {
+                    n || (n = !0, a(t, e))
+                }, function(e) {
+                    n || (n = !0, s(t, e))
+                })
+            } catch (e) {
+                if (n) return;
+                n = !0, s(t, e)
+            }
+        }
+        o.prototype.catch = function(e) {
+            return this.then(null, e)
+        }, o.prototype.then = function(e, t) {
+            var n = new this.constructor(r);
+            return i(this, new l(e, t, n)), n
+        }, o.prototype.finally = e.a, o.all = function(t) {
+            return new o(function(r, i) {
+                if (!t || void 0 === t.length) throw new TypeError("Promise.all accepts an array");
+                var o = Array.prototype.slice.call(t);
+                if (0 === o.length) return r([]);
+                var a = o.length;
+
+                function s(t, e) {
+                    try {
+                        if (e && ("object" == typeof e || "function" == typeof e)) {
+                            var n = e.then;
+                            if ("function" == typeof n) return void n.call(e, function(e) {
+                                s(t, e)
+                            }, i)
+                        }
+                        o[t] = e, 0 == --a && r(o)
+                    } catch (e) {
+                        i(e)
+                    }
+                }
+                for (var e = 0; e < o.length; e++) s(e, o[e])
+            })
+        }, o.resolve = function(t) {
+            return t && "object" == typeof t && t.constructor === o ? t : new o(function(e) {
+                e(t)
+            })
+        }, o.reject = function(n) {
+            return new o(function(e, t) {
+                t(n)
+            })
+        }, o.race = function(i) {
+            return new o(function(e, t) {
+                for (var n = 0, r = i.length; n < r; n++) i[n].then(e, t)
+            })
+        }, o._immediateFn = "function" == typeof t && function(e) {
+            t(e)
+        } || function(e) {
+            n(e, 0)
+        }, o._unhandledRejectionFn = function(e) {
+            "undefined" != typeof console && console && console.warn("Possible Unhandled Promise Rejection:", e)
+        }, f.a = o
+    }).call(this, d(21).setImmediate)
+}, function(e, t, n) {
+    "use strict";
+
+    function r(e, t) {
+        var n = document.createElement(e);
+        t && Array.prototype.forEach.call(Object.keys(t), function(e) {
+            n.setAttribute(e, t[e])
+        });
+        for (var r = arguments.length, i = new Array(2 < r ? r - 2 : 0), o = 2; o < r; o++) i[o - 2] = arguments[o];
+        return function t(e) {
+            Array.prototype.forEach.call(e, function(e) {
+                "string" == typeof e || "number" == typeof e ? n.textContent += e : Array.isArray(e) ? t(e) : void 0 !== e.__html ? n.innerHTML += e.__html : e instanceof Node && n.appendChild(e)
+            })
+        }(i), n
+    }
+    n.r(t), n.d(t, "createElement", function() {
+        return r
+    })
+}, function(e, t) {
+    var n;
+    n = function() {
+        return this
+    }();
+    try {
+        n = n || new Function("return this")()
+    } catch (e) {
+        "object" == typeof window && (n = window)
+    }
+    e.exports = n
+}, function(e, t, n) {
+    var r;
+    r = function() {
+        return function(n) {
+            var r = {};
+
+            function i(e) {
+                if (r[e]) return r[e].exports;
+                var t = r[e] = {
+                    i: e,
+                    l: !1,
+                    exports: {}
+                };
+                return n[e].call(t.exports, t, t.exports, i), t.l = !0, t.exports
+            }
+            return i.m = n, i.c = r, i.d = function(e, t, n) {
+                i.o(e, t) || Object.defineProperty(e, t, {
+                    enumerable: !0,
+                    get: n
+                })
+            }, i.r = function(e) {
+                "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
+                    value: "Module"
+                }), Object.defineProperty(e, "__esModule", {
+                    value: !0
+                })
+            }, i.t = function(t, e) {
+                if (1 & e && (t = i(t)), 8 & e) return t;
+                if (4 & e && "object" == typeof t && t && t.__esModule) return t;
+                var n = Object.create(null);
+                if (i.r(n), Object.defineProperty(n, "default", {
+                        enumerable: !0,
+                        value: t
+                    }), 2 & e && "string" != typeof t)
+                    for (var r in t) i.d(n, r, function(e) {
+                        return t[e]
+                    }.bind(null, r));
+                return n
+            }, i.n = function(e) {
+                var t = e && e.__esModule ? function() {
+                    return e.default
+                } : function() {
+                    return e
+                };
+                return i.d(t, "a", t), t
+            }, i.o = function(e, t) {
+                return Object.prototype.hasOwnProperty.call(e, t)
+            }, i.p = "", i(i.s = 0)
+        }([function(e, t, n) {
+            "use strict";
+            var i = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(e) {
+                    return typeof e
+                } : function(e) {
+                    return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
+                },
+                o = function() {
+                    function r(e, t) {
+                        for (var n = 0; n < t.length; n++) {
+                            var r = t[n];
+                            r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r)
+                        }
+                    }
+                    return function(e, t, n) {
+                        return t && r(e.prototype, t), n && r(e, n), e
+                    }
+                }(),
+                a = r(n(1)),
+                s = r(n(3)),
+                c = r(n(4));
+
+            function r(e) {
+                return e && e.__esModule ? e : {
+                    default: e
+                }
+            }
+            var l = function(e) {
+                function r(e, t) {
+                    ! function(e, t) {
+                        if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
+                    }(this, r);
+                    var n = function(e, t) {
+                        if (!e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+                        return !t || "object" != typeof t && "function" != typeof t ? e : t
+                    }(this, (r.__proto__ || Object.getPrototypeOf(r)).call(this));
+                    return n.resolveOptions(t), n.listenClick(e), n
+                }
+                return function(e, t) {
+                    if ("function" != typeof t && null !== t) throw new TypeError("Super expression must either be null or a function, not " + typeof t);
+                    e.prototype = Object.create(t && t.prototype, {
+                        constructor: {
+                            value: e,
+                            enumerable: !1,
+                            writable: !0,
+                            configurable: !0
+                        }
+                    }), t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : e.__proto__ = t)
+                }(r, s.default), o(r, [{
+                    key: "resolveOptions",
+                    value: function() {
+                        var e = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {};
+                        this.action = "function" == typeof e.action ? e.action : this.defaultAction, this.target = "function" == typeof e.target ? e.target : this.defaultTarget, this.text = "function" == typeof e.text ? e.text : this.defaultText, this.container = "object" === i(e.container) ? e.container : document.body
+                    }
+                }, {
+                    key: "listenClick",
+                    value: function(e) {
+                        var t = this;
+                        this.listener = (0, c.default)(e, "click", function(e) {
+                            return t.onClick(e)
+                        })
+                    }
+                }, {
+                    key: "onClick",
+                    value: function(e) {
+                        var t = e.delegateTarget || e.currentTarget;
+                        this.clipboardAction && (this.clipboardAction = null), this.clipboardAction = new a.default({
+                            action: this.action(t),
+                            target: this.target(t),
+                            text: this.text(t),
+                            container: this.container,
+                            trigger: t,
+                            emitter: this
+                        })
+                    }
+                }, {
+                    key: "defaultAction",
+                    value: function(e) {
+                        return u("action", e)
+                    }
+                }, {
+                    key: "defaultTarget",
+                    value: function(e) {
+                        var t = u("target", e);
+                        if (t) return document.querySelector(t)
+                    }
+                }, {
+                    key: "defaultText",
+                    value: function(e) {
+                        return u("text", e)
+                    }
+                }, {
+                    key: "destroy",
+                    value: function() {
+                        this.listener.destroy(), this.clipboardAction && (this.clipboardAction.destroy(), this.clipboardAction = null)
+                    }
+                }], [{
+                    key: "isSupported",
+                    value: function() {
+                        var e = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : ["copy", "cut"],
+                            t = "string" == typeof e ? [e] : e,
+                            n = !!document.queryCommandSupported;
+                        return t.forEach(function(e) {
+                            n = n && !!document.queryCommandSupported(e)
+                        }), n
+                    }
+                }]), r
+            }();
+
+            function u(e, t) {
+                var n = "data-clipboard-" + e;
+                if (t.hasAttribute(n)) return t.getAttribute(n)
+            }
+            e.exports = l
+        }, function(e, t, n) {
+            "use strict";
+            var r, i = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(e) {
+                    return typeof e
+                } : function(e) {
+                    return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
+                },
+                o = function() {
+                    function r(e, t) {
+                        for (var n = 0; n < t.length; n++) {
+                            var r = t[n];
+                            r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r)
+                        }
+                    }
+                    return function(e, t, n) {
+                        return t && r(e.prototype, t), n && r(e, n), e
+                    }
+                }(),
+                a = n(2),
+                s = (r = a) && r.__esModule ? r : {
+                    default: r
+                };
+            var c = function() {
+                function t(e) {
+                    ! function(e, t) {
+                        if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
+                    }(this, t), this.resolveOptions(e), this.initSelection()
+                }
+                return o(t, [{
+                    key: "resolveOptions",
+                    value: function() {
+                        var e = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {};
+                        this.action = e.action, this.container = e.container, this.emitter = e.emitter, this.target = e.target, this.text = e.text, this.trigger = e.trigger, this.selectedText = ""
+                    }
+                }, {
+                    key: "initSelection",
+                    value: function() {
+                        this.text ? this.selectFake() : this.target && this.selectTarget()
+                    }
+                }, {
+                    key: "selectFake",
+                    value: function() {
+                        var e = this,
+                            t = "rtl" == document.documentElement.getAttribute("dir");
+                        this.removeFake(), this.fakeHandlerCallback = function() {
+                            return e.removeFake()
+                        }, this.fakeHandler = this.container.addEventListener("click", this.fakeHandlerCallback) || !0, this.fakeElem = document.createElement("textarea"), this.fakeElem.style.fontSize = "12pt", this.fakeElem.style.border = "0", this.fakeElem.style.padding = "0", this.fakeElem.style.margin = "0", this.fakeElem.style.position = "absolute", this.fakeElem.style[t ? "right" : "left"] = "-9999px";
+                        var n = window.pageYOffset || document.documentElement.scrollTop;
+                        this.fakeElem.style.top = n + "px", this.fakeElem.setAttribute("readonly", ""), this.fakeElem.value = this.text, this.container.appendChild(this.fakeElem), this.selectedText = (0, s.default)(this.fakeElem), this.copyText()
+                    }
+                }, {
+                    key: "removeFake",
+                    value: function() {
+                        this.fakeHandler && (this.container.removeEventListener("click", this.fakeHandlerCallback), this.fakeHandler = null, this.fakeHandlerCallback = null), this.fakeElem && (this.container.removeChild(this.fakeElem), this.fakeElem = null)
+                    }
+                }, {
+                    key: "selectTarget",
+                    value: function() {
+                        this.selectedText = (0, s.default)(this.target), this.copyText()
+                    }
+                }, {
+                    key: "copyText",
+                    value: function() {
+                        var t = void 0;
+                        try {
+                            t = document.execCommand(this.action)
+                        } catch (e) {
+                            t = !1
+                        }
+                        this.handleResult(t)
+                    }
+                }, {
+                    key: "handleResult",
+                    value: function(e) {
+                        this.emitter.emit(e ? "success" : "error", {
+                            action: this.action,
+                            text: this.selectedText,
+                            trigger: this.trigger,
+                            clearSelection: this.clearSelection.bind(this)
+                        })
+                    }
+                }, {
+                    key: "clearSelection",
+                    value: function() {
+                        this.trigger && this.trigger.focus(), window.getSelection().removeAllRanges()
+                    }
+                }, {
+                    key: "destroy",
+                    value: function() {
+                        this.removeFake()
+                    }
+                }, {
+                    key: "action",
+                    set: function() {
+                        var e = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : "copy";
+                        if (this._action = e, "copy" !== this._action && "cut" !== this._action) throw new Error('Invalid "action" value, use either "copy" or "cut"')
+                    },
+                    get: function() {
+                        return this._action
+                    }
+                }, {
+                    key: "target",
+                    set: function(e) {
+                        if (void 0 !== e) {
+                            if (!e || "object" !== (void 0 === e ? "undefined" : i(e)) || 1 !== e.nodeType) throw new Error('Invalid "target" value, use a valid Element');
+                            if ("copy" === this.action && e.hasAttribute("disabled")) throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
+                            if ("cut" === this.action && (e.hasAttribute("readonly") || e.hasAttribute("disabled"))) throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
+                            this._target = e
+                        }
+                    },
+                    get: function() {
+                        return this._target
+                    }
+                }]), t
+            }();
+            e.exports = c
+        }, function(e, t) {
+            e.exports = function(e) {
+                var t;
+                if ("SELECT" === e.nodeName) e.focus(), t = e.value;
+                else if ("INPUT" === e.nodeName || "TEXTAREA" === e.nodeName) {
+                    var n = e.hasAttribute("readonly");
+                    n || e.setAttribute("readonly", ""), e.select(), e.setSelectionRange(0, e.value.length), n || e.removeAttribute("readonly"), t = e.value
+                } else {
+                    e.hasAttribute("contenteditable") && e.focus();
+                    var r = window.getSelection(),
+                        i = document.createRange();
+                    i.selectNodeContents(e), r.removeAllRanges(), r.addRange(i), t = r.toString()
+                }
+                return t
+            }
+        }, function(e, t) {
+            function n() {}
+            n.prototype = {
+                on: function(e, t, n) {
+                    var r = this.e || (this.e = {});
+                    return (r[e] || (r[e] = [])).push({
+                        fn: t,
+                        ctx: n
+                    }), this
+                },
+                once: function(e, t, n) {
+                    var r = this;
+
+                    function i() {
+                        r.off(e, i), t.apply(n, arguments)
+                    }
+                    return i._ = t, this.on(e, i, n)
+                },
+                emit: function(e) {
+                    for (var t = [].slice.call(arguments, 1), n = ((this.e || (this.e = {}))[e] || []).slice(), r = 0, i = n.length; r < i; r++) n[r].fn.apply(n[r].ctx, t);
+                    return this
+                },
+                off: function(e, t) {
+                    var n = this.e || (this.e = {}),
+                        r = n[e],
+                        i = [];
+                    if (r && t)
+                        for (var o = 0, a = r.length; o < a; o++) r[o].fn !== t && r[o].fn._ !== t && i.push(r[o]);
+                    return i.length ? n[e] = i : delete n[e], this
+                }
+            }, e.exports = n
+        }, function(e, t, n) {
+            var d = n(5),
+                h = n(6);
+            e.exports = function(e, t, n) {
+                if (!e && !t && !n) throw new Error("Missing required arguments");
+                if (!d.string(t)) throw new TypeError("Second argument must be a String");
+                if (!d.fn(n)) throw new TypeError("Third argument must be a Function");
+                if (d.node(e)) return u = t, f = n, (l = e).addEventListener(u, f), {
+                    destroy: function() {
+                        l.removeEventListener(u, f)
+                    }
+                };
+                if (d.nodeList(e)) return a = e, s = t, c = n, Array.prototype.forEach.call(a, function(e) {
+                    e.addEventListener(s, c)
+                }), {
+                    destroy: function() {
+                        Array.prototype.forEach.call(a, function(e) {
+                            e.removeEventListener(s, c)
+                        })
+                    }
+                };
+                if (d.string(e)) return r = e, i = t, o = n, h(document.body, r, i, o);
+                throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList");
+                var r, i, o, a, s, c, l, u, f
+            }
+        }, function(e, n) {
+            n.node = function(e) {
+                return void 0 !== e && e instanceof HTMLElement && 1 === e.nodeType
+            }, n.nodeList = function(e) {
+                var t = Object.prototype.toString.call(e);
+                return void 0 !== e && ("[object NodeList]" === t || "[object HTMLCollection]" === t) && "length" in e && (0 === e.length || n.node(e[0]))
+            }, n.string = function(e) {
+                return "string" == typeof e || e instanceof String
+            }, n.fn = function(e) {
+                return "[object Function]" === Object.prototype.toString.call(e)
+            }
+        }, function(e, t, n) {
+            var a = n(7);
+
+            function o(e, t, n, r, i) {
+                var o = function(t, n, e, r) {
+                    return function(e) {
+                        e.delegateTarget = a(e.target, n), e.delegateTarget && r.call(t, e)
+                    }
+                }.apply(this, arguments);
+                return e.addEventListener(n, o, i), {
+                    destroy: function() {
+                        e.removeEventListener(n, o, i)
+                    }
+                }
+            }
+            e.exports = function(e, t, n, r, i) {
+                return "function" == typeof e.addEventListener ? o.apply(null, arguments) : "function" == typeof n ? o.bind(null, document).apply(null, arguments) : ("string" == typeof e && (e = document.querySelectorAll(e)), Array.prototype.map.call(e, function(e) {
+                    return o(e, t, n, r, i)
+                }))
+            }
+        }, function(e, t) {
+            if ("undefined" != typeof Element && !Element.prototype.matches) {
+                var n = Element.prototype;
+                n.matches = n.matchesSelector || n.mozMatchesSelector || n.msMatchesSelector || n.oMatchesSelector || n.webkitMatchesSelector
+            }
+            e.exports = function(e, t) {
+                for (; e && 9 !== e.nodeType;) {
+                    if ("function" == typeof e.matches && e.matches(t)) return e;
+                    e = e.parentNode
+                }
+            }
+        }])
+    }, e.exports = r()
+}, function(r, i, o) {
+    var a, s;
+    ! function(e) {
+        if (void 0 === (s = "function" == typeof(a = e) ? a.call(i, o, i, r) : a) || (r.exports = s), !0, r.exports = e(), !!0) {
+            var t = window.Cookies,
+                n = window.Cookies = e();
+            n.noConflict = function() {
+                return window.Cookies = t, n
+            }
+        }
+    }(function() {
+        function m() {
+            for (var e = 0, t = {}; e < arguments.length; e++) {
+                var n = arguments[e];
+                for (var r in n) t[r] = n[r]
+            }
+            return t
+        }
+        return function e(h) {
+            function p(e, t, n) {
+                var r;
+                if ("undefined" != typeof document) {
+                    if (1 < arguments.length) {
+                        if ("number" == typeof(n = m({
+                                path: "/"
+                            }, p.defaults, n)).expires) {
+                            var i = new Date;
+                            i.setMilliseconds(i.getMilliseconds() + 864e5 * n.expires), n.expires = i
+                        }
+                        n.expires = n.expires ? n.expires.toUTCString() : "";
+                        try {
+                            r = JSON.stringify(t), /^[\{\[]/.test(r) && (t = r)
+                        } catch (e) {}
+                        t = h.write ? h.write(t, e) : encodeURIComponent(String(t)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent), e = (e = (e = encodeURIComponent(String(e))).replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)).replace(/[\(\)]/g, escape);
+                        var o = "";
+                        for (var a in n) n[a] && (o += "; " + a, !0 !== n[a] && (o += "=" + n[a]));
+                        return document.cookie = e + "=" + t + o
+                    }
+                    e || (r = {});
+                    for (var s = document.cookie ? document.cookie.split("; ") : [], c = /(%[0-9A-Z]{2})+/g, l = 0; l < s.length; l++) {
+                        var u = s[l].split("="),
+                            f = u.slice(1).join("=");
+                        this.json || '"' !== f.charAt(0) || (f = f.slice(1, -1));
+                        try {
+                            var d = u[0].replace(c, decodeURIComponent);
+                            if (f = h.read ? h.read(f, d) : h(f, d) || f.replace(c, decodeURIComponent), this.json) try {
+                                f = JSON.parse(f)
+                            } catch (e) {}
+                            if (e === d) {
+                                r = f;
+                                break
+                            }
+                            e || (r[d] = f)
+                        } catch (e) {}
+                    }
+                    return r
+                }
+            }
+            return (p.set = p).get = function(e) {
+                return p.call(p, e)
+            }, p.getJSON = function() {
+                return p.apply({
+                    json: !0
+                }, [].slice.call(arguments))
+            }, p.defaults = {}, p.remove = function(e, t) {
+                p(e, "", m(t, {
+                    expires: -1
+                }))
+            }, p.withConverter = e, p
+        }(function() {})
+    })
+}, function(e, t, n) {
+    "use strict";
+    n.r(t);
+    var r = "function" == typeof fetch ? fetch.bind() : function(i, o) {
+        return o = o || {}, new Promise(function(e, t) {
+            var n = new XMLHttpRequest;
+            for (var r in n.open(o.method || "get", i, !0), o.headers) n.setRequestHeader(r, o.headers[r]);
+
+            function s() {
+                var r, i = [],
+                    o = [],
+                    a = {};
+                return n.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, function(e, t, n) {
+                    i.push(t = t.toLowerCase()), o.push([t, n]), r = a[t], a[t] = r ? r + "," + n : n
+                }), {
+                    ok: 2 == (n.status / 100 | 0),
+                    status: n.status,
+                    statusText: n.statusText,
+                    url: n.responseURL,
+                    clone: s,
+                    text: function() {
+                        return Promise.resolve(n.responseText)
+                    },
+                    json: function() {
+                        return Promise.resolve(n.responseText).then(JSON.parse)
+                    },
+                    blob: function() {
+                        return Promise.resolve(new Blob([n.response]))
+                    },
+                    headers: {
+                        keys: function() {
+                            return i
+                        },
+                        entries: function() {
+                            return o
+                        },
+                        get: function(e) {
+                            return a[e.toLowerCase()]
+                        },
+                        has: function(e) {
+                            return e.toLowerCase() in a
+                        }
+                    }
+                }
+            }
+            n.withCredentials = "include" == o.credentials, n.onload = function() {
+                e(s())
+            }, n.onerror = t, n.send(o.body || null)
+        })
+    };
+    t.default = r
+}, function(e, t, n) {
+    "use strict";
+    t.a = function(t) {
+        var n = this.constructor;
+        return this.then(function(e) {
+            return n.resolve(t()).then(function() {
+                return e
+            })
+        }, function(e) {
+            return n.resolve(t()).then(function() {
+                return n.reject(e)
+            })
+        })
+    }
+}, function(e, n, r) {
+    "use strict";
+    (function(f) {
+        r.d(n, "a", function() {
+            return t
+        });
+        var e = r(1),
+            d = r.n(e),
+            h = function(e) {
+                var t = document.getElementsByName("lang:" + e)[0];
+                if (!(t instanceof HTMLMetaElement)) throw new ReferenceError;
+                return t.content
+            },
+            t = function() {
+                function e(e, t) {
+                    var n = "string" == typeof e ? document.querySelector(e) : e;
+                    if (!(n instanceof HTMLElement)) throw new ReferenceError;
+                    this.el_ = n;
+                    var r = Array.prototype.slice.call(this.el_.children),
+                        i = r[0],
+                        o = r[1];
+                    this.data_ = t, this.meta_ = i, this.list_ = o, this.message_ = {
+                        placeholder: this.meta_.textContent,
+                        none: h("search.result.none"),
+                        one: h("search.result.one"),
+                        other: h("search.result.other")
+                    };
+                    var a = h("search.tokenizer");
+                    a.length && (d.a.tokenizer.separator = a), this.lang_ = h("search.language").split(",").filter(Boolean).map(function(e) {
+                        return e.trim()
+                    })
+                }
+                return e.prototype.update = function(e) {
+                    var t, a = this;
+                    if ("focus" !== e.type || this.index_) {
+                        if ("focus" === e.type || "keyup" === e.type) {
+                            var n = e.target;
+                            if (!(n instanceof HTMLInputElement)) throw new ReferenceError;
+                            if (!this.index_ || n.value === this.value_) return;
+                            for (; this.list_.firstChild;) this.list_.removeChild(this.list_.firstChild);
+                            if (this.value_ = n.value, 0 === this.value_.length) return void(this.meta_.textContent = this.message_.placeholder);
+                            var r = this.index_.query(function(t) {
+                                    a.value_.toLowerCase().split(" ").filter(Boolean).forEach(function(e) {
+                                        t.term(e, {
+                                            wildcard: d.a.Query.wildcard.TRAILING
+                                        })
+                                    })
+                                }).reduce(function(e, t) {
+                                    var n = a.docs_.get(t.ref);
+                                    if (n.parent) {
+                                        var r = n.parent.location;
+                                        e.set(r, (e.get(r) || []).concat(t))
+                                    } else {
+                                        var i = n.location;
+                                        e.set(i, e.get(i) || [])
+                                    }
+                                    return e
+                                }, new Map),
+                                i = (t = this.value_.trim(), t.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")).replace(new RegExp(d.a.tokenizer.separator, "img"), "|"),
+                                s = new RegExp("(^|" + d.a.tokenizer.separator + ")(" + i + ")", "img"),
+                                c = function(e, t, n) {
+                                    return t + "<em>" + n + "</em>"
+                                };
+                            this.stack_ = [], r.forEach(function(e, t) {
+                                var n, r = a.docs_.get(t),
+                                    i = f.createElement("li", {
+                                        class: "md-search-result__item"
+                                    }, f.createElement("a", {
+                                        href: r.location,
+                                        title: r.title,
+                                        class: "md-search-result__link",
+                                        tabindex: "-1"
+                                    }, f.createElement("article", {
+                                        class: "md-search-result__article md-search-result__article--document"
+                                    }, f.createElement("h1", {
+                                        class: "md-search-result__title"
+                                    }, {
+                                        __html: r.title.replace(s, c)
+                                    }), r.text.length ? f.createElement("p", {
+                                        class: "md-search-result__teaser"
+                                    }, {
+                                        __html: r.text.replace(s, c)
+                                    }) : {}))),
+                                    o = e.map(function(t) {
+                                        return function() {
+                                            var e = a.docs_.get(t.ref);
+                                            i.appendChild(f.createElement("a", {
+                                                href: e.location,
+                                                title: e.title,
+                                                class: "md-search-result__link",
+                                                "data-md-rel": "anchor",
+                                                tabindex: "-1"
+                                            }, f.createElement("article", {
+                                                class: "md-search-result__article"
+                                            }, f.createElement("h1", {
+                                                class: "md-search-result__title"
+                                            }, {
+                                                __html: e.title.replace(s, c)
+                                            }), e.text.length ? f.createElement("p", {
+                                                class: "md-search-result__teaser"
+                                            }, {
+                                                __html: function(e, t) {
+                                                    var n = t;
+                                                    if (e.length > n) {
+                                                        for (;
+                                                            " " !== e[n] && 0 < --n;);
+                                                        return e.substring(0, n) + "..."
+                                                    }
+                                                    return e
+                                                }(e.text.replace(s, c), 400)
+                                            }) : {})))
+                                        }
+                                    });
+                                (n = a.stack_).push.apply(n, [function() {
+                                    return a.list_.appendChild(i)
+                                }].concat(o))
+                            });
+                            var o = this.el_.parentNode;
+                            if (!(o instanceof HTMLElement)) throw new ReferenceError;
+                            for (; this.stack_.length && o.offsetHeight >= o.scrollHeight - 16;) this.stack_.shift()();
+                            var l = this.list_.querySelectorAll("[data-md-rel=anchor]");
+                            switch (Array.prototype.forEach.call(l, function(r) {
+                                ["click", "keydown"].forEach(function(n) {
+                                    r.addEventListener(n, function(e) {
+                                        if ("keydown" !== n || 13 === e.keyCode) {
+                                            var t = document.querySelector("[data-md-toggle=search]");
+                                            if (!(t instanceof HTMLInputElement)) throw new ReferenceError;
+                                            t.checked && (t.checked = !1, t.dispatchEvent(new CustomEvent("change"))), e.preventDefault(), setTimeout(function() {
+                                                document.location.href = r.href
+                                            }, 100)
+                                        }
+                                    })
+                                })
+                            }), r.size) {
+                                case 0:
+                                    this.meta_.textContent = this.message_.none;
+                                    break;
+                                case 1:
+                                    this.meta_.textContent = this.message_.one;
+                                    break;
+                                default:
+                                    this.meta_.textContent = this.message_.other.replace("#", r.size)
+                            }
+                        }
+                    } else {
+                        var u = function(e) {
+                            a.docs_ = e.reduce(function(e, t) {
+                                var n, r, i, o = t.location.split("#"),
+                                    a = o[0],
+                                    s = o[1];
+                                return t.text = (n = t.text, r = document.createTextNode(n), (i = document.createElement("p")).appendChild(r), i.innerHTML), s && (t.parent = e.get(a), t.parent && !t.parent.done && (t.parent.title = t.title, t.parent.text = t.text, t.parent.done = !0)), t.text = t.text.replace(/\n/g, " ").replace(/\s+/g, " ").replace(/\s+([,.:;!?])/g, function(e, t) {
+                                    return t
+                                }), t.parent && t.parent.title === t.title || e.set(t.location, t), e
+                            }, new Map);
+                            var i = a.docs_,
+                                o = a.lang_;
+                            a.stack_ = [], a.index_ = d()(function() {
+                                var e, t = this,
+                                    n = {
+                                        "search.pipeline.trimmer": d.a.trimmer,
+                                        "search.pipeline.stopwords": d.a.stopWordFilter
+                                    },
+                                    r = Object.keys(n).reduce(function(e, t) {
+                                        return h(t).match(/^false$/i) || e.push(n[t]), e
+                                    }, []);
+                                this.pipeline.reset(), r && (e = this.pipeline).add.apply(e, r), 1 === o.length && "en" !== o[0] && d.a[o[0]] ? this.use(d.a[o[0]]) : 1 < o.length && this.use(d.a.multiLanguage.apply(d.a, o)), this.field("title", {
+                                    boost: 10
+                                }), this.field("text"), this.ref("location"), i.forEach(function(e) {
+                                    return t.add(e)
+                                })
+                            });
+                            var t = a.el_.parentNode;
+                            if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                            t.addEventListener("scroll", function() {
+                                for (; a.stack_.length && t.scrollTop + t.offsetHeight >= t.scrollHeight - 16;) a.stack_.splice(0, 10).forEach(function(e) {
+                                    return e()
+                                })
+                            })
+                        };
+                        setTimeout(function() {
+                            return "function" == typeof a.data_ ? a.data_().then(u) : u(a.data_)
+                        }, 250)
+                    }
+                }, e
+            }()
+    }).call(this, r(3))
+}, function(e, n, r) {
+    "use strict";
+    (function(t) {
+        r.d(n, "a", function() {
+            return e
+        });
+        var e = function() {
+            function e(e) {
+                var t = "string" == typeof e ? document.querySelector(e) : e;
+                if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                this.el_ = t
+            }
+            return e.prototype.initialize = function(e) {
+                e.length && this.el_.children.length && this.el_.children[this.el_.children.length - 1].appendChild(t.createElement("ul", {
+                    class: "md-source__facts"
+                }, e.map(function(e) {
+                    return t.createElement("li", {
+                        class: "md-source__fact"
+                    }, e)
+                }))), this.el_.dataset.mdState = "done"
+            }, e
+        }()
+    }).call(this, r(3))
+}, , , function(e, n, c) {
+    "use strict";
+    c.r(n),
+        function(o) {
+            c.d(n, "app", function() {
+                return t
+            });
+            c(14), c(15), c(16), c(17), c(18), c(19), c(20);
+            var r = c(2),
+                e = c(5),
+                a = c.n(e),
+                i = c(0);
+            window.Promise = window.Promise || r.a;
+            var s = function(e) {
+                var t = document.getElementsByName("lang:" + e)[0];
+                if (!(t instanceof HTMLMetaElement)) throw new ReferenceError;
+                return t.content
+            };
+            var t = {
+                initialize: function(t) {
+                    new i.a.Event.Listener(document, "DOMContentLoaded", function() {
+                        if (!(document.body instanceof HTMLElement)) throw new ReferenceError;
+                        Modernizr.addTest("ios", function() {
+                            return !!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
+                        });
+                        var e = document.querySelectorAll("table:not([class])");
+                        if (Array.prototype.forEach.call(e, function(e) {
+                                var t = o.createElement("div", {
+                                    class: "md-typeset__scrollwrap"
+                                }, o.createElement("div", {
+                                    class: "md-typeset__table"
+                                }));
+                                e.nextSibling ? e.parentNode.insertBefore(t, e.nextSibling) : e.parentNode.appendChild(t), t.children[0].appendChild(e)
+                            }), a.a.isSupported()) {
+                            var t = document.querySelectorAll(".codehilite > pre, pre > code");
+                            Array.prototype.forEach.call(t, function(e, t) {
+                                var n = "__code_" + t,
+                                    r = o.createElement("button", {
+                                        class: "md-clipboard",
+                                        title: s("clipboard.copy"),
+                                        "data-clipboard-target": "#" + n + " pre, #" + n + " code"
+                                    }, o.createElement("span", {
+                                        class: "md-clipboard__message"
+                                    })),
+                                    i = e.parentNode;
+                                i.id = n, i.insertBefore(r, e)
+                            }), new a.a(".md-clipboard").on("success", function(e) {
+                                var t = e.trigger.querySelector(".md-clipboard__message");
+                                if (!(t instanceof HTMLElement)) throw new ReferenceError;
+                                e.clearSelection(), t.dataset.mdTimer && clearTimeout(parseInt(t.dataset.mdTimer, 10)), t.classList.add("md-clipboard__message--active"), t.innerHTML = s("clipboard.copied"), t.dataset.mdTimer = setTimeout(function() {
+                                    t.classList.remove("md-clipboard__message--active"), t.dataset.mdTimer = ""
+                                }, 2e3).toString()
+                            })
+                        }
+                        if (!Modernizr.details) {
+                            var n = document.querySelectorAll("details > summary");
+                            Array.prototype.forEach.call(n, function(e) {
+                                e.addEventListener("click", function(e) {
+                                    var t = e.target.parentNode;
+                                    t.hasAttribute("open") ? t.removeAttribute("open") : t.setAttribute("open", "")
+                                })
+                            })
+                        }
+                        var r = function() {
+                            if (document.location.hash) {
+                                var e = document.getElementById(document.location.hash.substring(1));
+                                if (!e) return;
+                                for (var t = e.parentNode; t && !(t instanceof HTMLDetailsElement);) t = t.parentNode;
+                                if (t && !t.open) {
+                                    t.open = !0;
+                                    var n = location.hash;
+                                    location.hash = " ", location.hash = n
+                                }
+                            }
+                        };
+                        if (window.addEventListener("hashchange", r), r(), Modernizr.ios) {
+                            var i = document.querySelectorAll("[data-md-scrollfix]");
+                            Array.prototype.forEach.call(i, function(t) {
+                                t.addEventListener("touchstart", function() {
+                                    var e = t.scrollTop;
+                                    0 === e ? t.scrollTop = 1 : e + t.offsetHeight === t.scrollHeight && (t.scrollTop = e - 1)
+                                })
+                            })
+                        }
+                    }).listen(), new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Header.Shadow("[data-md-component=container]", "[data-md-component=header]")).listen(), new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Header.Title("[data-md-component=title]", ".md-typeset h1")).listen(), document.querySelector("[data-md-component=hero]") && new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Tabs.Toggle("[data-md-component=hero]")).listen(), document.querySelector("[data-md-component=tabs]") && new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Tabs.Toggle("[data-md-component=tabs]")).listen(), new i.a.Event.MatchMedia("(min-width: 1220px)", new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Sidebar.Position("[data-md-component=navigation]", "[data-md-component=header]"))), document.querySelector("[data-md-component=toc]") && new i.a.Event.MatchMedia("(min-width: 960px)", new i.a.Event.Listener(window, ["scroll", "resize", "orientationchange"], new i.a.Sidebar.Position("[data-md-component=toc]", "[data-md-component=header]"))), new i.a.Event.MatchMedia("(min-width: 960px)", new i.a.Event.Listener(window, "scroll", new i.a.Nav.Blur("[data-md-component=toc] .md-nav__link")));
+                    var e = document.querySelectorAll("[data-md-component=collapsible]");
+                    Array.prototype.forEach.call(e, function(e) {
+                            new i.a.Event.MatchMedia("(min-width: 1220px)", new i.a.Event.Listener(e.previousElementSibling, "click", new i.a.Nav.Collapse(e)))
+                        }), new i.a.Event.MatchMedia("(max-width: 1219px)", new i.a.Event.Listener("[data-md-component=navigation] [data-md-toggle]", "change", new i.a.Nav.Scrolling("[data-md-component=navigation] nav"))), document.querySelector("[data-md-component=search]") && (new i.a.Event.MatchMedia("(max-width: 959px)", new i.a.Event.Listener("[data-md-toggle=search]", "change", new i.a.Search.Lock("[data-md-toggle=search]")))),
+                        new i.a.Event.Listener(document.body, "keydown", function(e) {
+                            if (9 === e.keyCode) {
+                                var t = document.querySelectorAll("[data-md-component=navigation] .md-nav__link[for]:not([tabindex])");
+                                Array.prototype.forEach.call(t, function(e) {
+                                    e.offsetHeight && (e.tabIndex = 0)
+                                })
+                            }
+                        }).listen(), new i.a.Event.Listener(document.body, "mousedown", function() {
+                            var e = document.querySelectorAll("[data-md-component=navigation] .md-nav__link[tabindex]");
+                            Array.prototype.forEach.call(e, function(e) {
+                                e.removeAttribute("tabIndex")
+                            })
+                        }).listen(), document.body.addEventListener("click", function() {
+                            "tabbing" === document.body.dataset.mdState && (document.body.dataset.mdState = "")
+                        }), new i.a.Event.MatchMedia("(max-width: 959px)", new i.a.Event.Listener("[data-md-component=navigation] [href^='#']", "click", function() {
+                            var e = document.querySelector("[data-md-toggle=drawer]");
+                            if (!(e instanceof HTMLInputElement)) throw new ReferenceError;
+                            e.checked && (e.checked = !1, e.dispatchEvent(new CustomEvent("change")))
+                        })),
+                        function() {
+                            var e = document.querySelector("[data-md-source]");
+                            if (!e) return r.a.resolve([]);
+                            if (!(e instanceof HTMLAnchorElement)) throw new ReferenceError;
+                            switch (e.dataset.mdSource) {
+                                case "github":
+                                    return new i.a.Source.Adapter.GitHub(e).fetch();
+                                default:
+                                    return r.a.resolve([])
+                            }
+                        }().then(function(t) {
+                            var e = document.querySelectorAll("[data-md-source]");
+                            Array.prototype.forEach.call(e, function(e) {
+                                new i.a.Source.Repository(e).initialize(t)
+                            })
+                        });
+                    var n = function() {
+                        var e = document.querySelectorAll("details");
+                        Array.prototype.forEach.call(e, function(e) {
+                            e.setAttribute("open", "")
+                        })
+                    };
+                    new i.a.Event.MatchMedia("print", {
+                        listen: n,
+                        unlisten: function() {}
+                    }), window.onbeforeprint = n
+                }
+            }
+        }.call(this, c(3))
+}, function(e, t, n) {
+    e.exports = n.p + "assets/images/icons/bitbucket.1b09e088.svg"
+}, function(e, t, n) {
+    e.exports = n.p + "assets/images/icons/github.f0b8504a.svg"
+}, function(e, t, n) {
+    e.exports = n.p + "assets/images/icons/gitlab.6dd19c00.svg"
+}, function(e, t) {
+    e.exports = "/Users/squidfunk/Desktop/General/Sources/mkdocs-material/material/application.4031d38b.css"
+}, function(e, t) {
+    e.exports = "/Users/squidfunk/Desktop/General/Sources/mkdocs-material/material/application-palette.224b79ff.css"
+}, function(e, t) {
+    ! function() {
+        if ("undefined" != typeof window) try {
+            var e = new window.CustomEvent("test", {
+                cancelable: !0
+            });
+            if (e.preventDefault(), !0 !== e.defaultPrevented) throw new Error("Could not prevent default")
+        } catch (e) {
+            var t = function(e, t) {
+                var n, r;
+                return (t = t || {}).bubbles = !!t.bubbles, t.cancelable = !!t.cancelable, (n = document.createEvent("CustomEvent")).initCustomEvent(e, t.bubbles, t.cancelable, t.detail), r = n.preventDefault, n.preventDefault = function() {
+                    r.call(this);
+                    try {
+                        Object.defineProperty(this, "defaultPrevented", {
+                            get: function() {
+                                return !0
+                            }
+                        })
+                    } catch (e) {
+                        this.defaultPrevented = !0
+                    }
+                }, n
+            };
+            t.prototype = window.Event.prototype, window.CustomEvent = t
+        }
+    }()
+}, function(e, t, n) {
+    window.fetch || (window.fetch = n(7).default || n(7))
+}, function(e, i, o) {
+    (function(e) {
+        var t = void 0 !== e && e || "undefined" != typeof self && self || window,
+            n = Function.prototype.apply;
+
+        function r(e, t) {
+            this._id = e, this._clearFn = t
+        }
+        i.setTimeout = function() {
+            return new r(n.call(setTimeout, t, arguments), clearTimeout)
+        }, i.setInterval = function() {
+            return new r(n.call(setInterval, t, arguments), clearInterval)
+        }, i.clearTimeout = i.clearInterval = function(e) {
+            e && e.close()
+        }, r.prototype.unref = r.prototype.ref = function() {}, r.prototype.close = function() {
+            this._clearFn.call(t, this._id)
+        }, i.enroll = function(e, t) {
+            clearTimeout(e._idleTimeoutId), e._idleTimeout = t
+        }, i.unenroll = function(e) {
+            clearTimeout(e._idleTimeoutId), e._idleTimeout = -1
+        }, i._unrefActive = i.active = function(e) {
+            clearTimeout(e._idleTimeoutId);
+            var t = e._idleTimeout;
+            0 <= t && (e._idleTimeoutId = setTimeout(function() {
+                e._onTimeout && e._onTimeout()
+            }, t))
+        }, o(22), i.setImmediate = "undefined" != typeof self && self.setImmediate || void 0 !== e && e.setImmediate || this && this.setImmediate, i.clearImmediate = "undefined" != typeof self && self.clearImmediate || void 0 !== e && e.clearImmediate || this && this.clearImmediate
+    }).call(this, o(4))
+}, function(e, t, n) {
+    (function(e, p) {
+        ! function(n, r) {
+            "use strict";
+            if (!n.setImmediate) {
+                var i, o, t, a, e, s = 1,
+                    c = {},
+                    l = !1,
+                    u = n.document,
+                    f = Object.getPrototypeOf && Object.getPrototypeOf(n);
+                f = f && f.setTimeout ? f : n, i = "[object process]" === {}.toString.call(n.process) ? function(e) {
+                    p.nextTick(function() {
+                        h(e)
+                    })
+                } : function() {
+                    if (n.postMessage && !n.importScripts) {
+                        var e = !0,
+                            t = n.onmessage;
+                        return n.onmessage = function() {
+                            e = !1
+                        }, n.postMessage("", "*"), n.onmessage = t, e
+                    }
+                }() ? (a = "setImmediate$" + Math.random() + "$", e = function(e) {
+                    e.source === n && "string" == typeof e.data && 0 === e.data.indexOf(a) && h(+e.data.slice(a.length))
+                }, n.addEventListener ? n.addEventListener("message", e, !1) : n.attachEvent("onmessage", e), function(e) {
+                    n.postMessage(a + e, "*")
+                }) : n.MessageChannel ? ((t = new MessageChannel).port1.onmessage = function(e) {
+                    h(e.data)
+                }, function(e) {
+                    t.port2.postMessage(e)
+                }) : u && "onreadystatechange" in u.createElement("script") ? (o = u.documentElement, function(e) {
+                    var t = u.createElement("script");
+                    t.onreadystatechange = function() {
+                        h(e), t.onreadystatechange = null, o.removeChild(t), t = null
+                    }, o.appendChild(t)
+                }) : function(e) {
+                    setTimeout(h, 0, e)
+                }, f.setImmediate = function(e) {
+                    "function" != typeof e && (e = new Function("" + e));
+                    for (var t = new Array(arguments.length - 1), n = 0; n < t.length; n++) t[n] = arguments[n + 1];
+                    var r = {
+                        callback: e,
+                        args: t
+                    };
+                    return c[s] = r, i(s), s++
+                }, f.clearImmediate = d
+            }
+
+            function d(e) {
+                delete c[e]
+            }
+
+            function h(e) {
+                if (l) setTimeout(h, 0, e);
+                else {
+                    var t = c[e];
+                    if (t) {
+                        l = !0;
+                        try {
+                            ! function(e) {
+                                var t = e.callback,
+                                    n = e.args;
+                                switch (n.length) {
+                                    case 0:
+                                        t();
+                                        break;
+                                    case 1:
+                                        t(n[0]);
+                                        break;
+                                    case 2:
+                                        t(n[0], n[1]);
+                                        break;
+                                    case 3:
+                                        t(n[0], n[1], n[2]);
+                                        break;
+                                    default:
+                                        t.apply(r, n)
+                                }
+                            }(t)
+                        } finally {
+                            d(e), l = !1
+                        }
+                    }
+                }
+            }
+        }("undefined" == typeof self ? void 0 === e ? this : e : self)
+    }).call(this, n(4), n(23))
+}, function(e, t) {
+    var n, r, i = e.exports = {};
+
+    function o() {
+        throw new Error("setTimeout has not been defined")
+    }
+
+    function a() {
+        throw new Error("clearTimeout has not been defined")
+    }
+
+    function s(t) {
+        if (n === setTimeout) return setTimeout(t, 0);
+        if ((n === o || !n) && setTimeout) return n = setTimeout, setTimeout(t, 0);
+        try {
+            return n(t, 0)
+        } catch (e) {
+            try {
+                return n.call(null, t, 0)
+            } catch (e) {
+                return n.call(this, t, 0)
+            }
+        }
+    }! function() {
+        try {
+            n = "function" == typeof setTimeout ? setTimeout : o
+        } catch (e) {
+            n = o
+        }
+        try {
+            r = "function" == typeof clearTimeout ? clearTimeout : a
+        } catch (e) {
+            r = a
+        }
+    }();
+    var c, l = [],
+        u = !1,
+        f = -1;
+
+    function d() {
+        u && c && (u = !1, c.length ? l = c.concat(l) : f = -1, l.length && h())
+    }
+
+    function h() {
+        if (!u) {
+            var e = s(d);
+            u = !0;
+            for (var t = l.length; t;) {
+                for (c = l, l = []; ++f < t;) c && c[f].run();
+                f = -1, t = l.length
+            }
+            c = null, u = !1,
+                function(t) {
+                    if (r === clearTimeout) return clearTimeout(t);
+                    if ((r === a || !r) && clearTimeout) return r = clearTimeout, clearTimeout(t);
+                    try {
+                        r(t)
+                    } catch (e) {
+                        try {
+                            return r.call(null, t)
+                        } catch (e) {
+                            return r.call(this, t)
+                        }
+                    }
+                }(e)
+        }
+    }
+
+    function p(e, t) {
+        this.fun = e, this.array = t
+    }
+
+    function m() {}
+    i.nextTick = function(e) {
+        var t = new Array(arguments.length - 1);
+        if (1 < arguments.length)
+            for (var n = 1; n < arguments.length; n++) t[n - 1] = arguments[n];
+        l.push(new p(e, t)), 1 !== l.length || u || s(h)
+    }, p.prototype.run = function() {
+        this.fun.apply(null, this.array)
+    }, i.title = "browser", i.browser = !0, i.env = {}, i.argv = [], i.version = "", i.versions = {}, i.on = m, i.addListener = m, i.once = m, i.off = m, i.removeListener = m, i.removeAllListeners = m, i.emit = m, i.prependListener = m, i.prependOnceListener = m, i.listeners = function(e) {
+        return []
+    }, i.binding = function(e) {
+        throw new Error("process.binding is not supported")
+    }, i.cwd = function() {
+        return "/"
+    }, i.chdir = function(e) {
+        throw new Error("process.chdir is not supported")
+    }, i.umask = function() {
+        return 0
+    }
+}, function(i, o, a) {
+    var s, c;
+    /**
+     * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.6
+     * Copyright (C) 2019 Oliver Nightingale
+     * @license MIT
+     */
+    ! function() {
+        var t, l, u, e, n, f, d, h, p, m, y, v, g, w, _, E, x, b, k, S, T, L, R, O, C, r, D = function(e) {
+            var t = new D.Builder;
+            return t.pipeline.add(D.trimmer, D.stopWordFilter, D.stemmer), t.searchPipeline.add(D.stemmer), e.call(t, t), t.build()
+        };
+        D.version = "2.3.6", D.utils = {}, D.utils.warn = (t = this, function(e) {
+            t.console && console.warn && console.warn(e)
+        }), D.utils.asString = function(e) {
+            return null == e ? "" : e.toString()
+        }, D.utils.clone = function(e) {
+            if (null == e) return e;
+            for (var t = Object.create(null), n = Object.keys(e), r = 0; r < n.length; r++) {
+                var i = n[r],
+                    o = e[i];
+                if (Array.isArray(o)) t[i] = o.slice();
+                else {
+                    if ("string" != typeof o && "number" != typeof o && "boolean" != typeof o) throw new TypeError("clone is not deep and does not support nested objects");
+                    t[i] = o
+                }
+            }
+            return t
+        }, D.FieldRef = function(e, t, n) {
+            this.docRef = e, this.fieldName = t, this._stringValue = n
+        }, D.FieldRef.joiner = "/", D.FieldRef.fromString = function(e) {
+            var t = e.indexOf(D.FieldRef.joiner);
+            if (-1 === t) throw "malformed field ref string";
+            var n = e.slice(0, t),
+                r = e.slice(t + 1);
+            return new D.FieldRef(r, n, e)
+        }, D.FieldRef.prototype.toString = function() {
+            return null == this._stringValue && (this._stringValue = this.fieldName + D.FieldRef.joiner + this.docRef), this._stringValue
+        }, D.Set = function(e) {
+            if (this.elements = Object.create(null), e) {
+                this.length = e.length;
+                for (var t = 0; t < this.length; t++) this.elements[e[t]] = !0
+            } else this.length = 0
+        }, D.Set.complete = {
+            intersect: function(e) {
+                return e
+            },
+            union: function(e) {
+                return e
+            },
+            contains: function() {
+                return !0
+            }
+        }, D.Set.empty = {
+            intersect: function() {
+                return this
+            },
+            union: function(e) {
+                return e
+            },
+            contains: function() {
+                return !1
+            }
+        }, D.Set.prototype.contains = function(e) {
+            return !!this.elements[e]
+        }, D.Set.prototype.intersect = function(e) {
+            var t, n, r, i = [];
+            if (e === D.Set.complete) return this;
+            if (e === D.Set.empty) return e;
+            n = this.length < e.length ? (t = this, e) : (t = e, this), r = Object.keys(t.elements);
+            for (var o = 0; o < r.length; o++) {
+                var a = r[o];
+                a in n.elements && i.push(a)
+            }
+            return new D.Set(i)
+        }, D.Set.prototype.union = function(e) {
+            return e === D.Set.complete ? D.Set.complete : e === D.Set.empty ? this : new D.Set(Object.keys(this.elements).concat(Object.keys(e.elements)))
+        }, D.idf = function(e, t) {
+            var n = 0;
+            for (var r in e) "_index" != r && (n += Object.keys(e[r]).length);
+            var i = (t - n + .5) / (n + .5);
+            return Math.log(1 + Math.abs(i))
+        }, D.Token = function(e, t) {
+            this.str = e || "", this.metadata = t || {}
+        }, D.Token.prototype.toString = function() {
+            return this.str
+        }, D.Token.prototype.update = function(e) {
+            return this.str = e(this.str, this.metadata), this
+        }, D.Token.prototype.clone = function(e) {
+            return e = e || function(e) {
+                return e
+            }, new D.Token(e(this.str, this.metadata), this.metadata)
+        }, D.tokenizer = function(e, t) {
+            if (null == e || null == e) return [];
+            if (Array.isArray(e)) return e.map(function(e) {
+                return new D.Token(D.utils.asString(e).toLowerCase(), D.utils.clone(t))
+            });
+            for (var n = e.toString().trim().toLowerCase(), r = n.length, i = [], o = 0, a = 0; o <= r; o++) {
+                var s = o - a;
+                if (n.charAt(o).match(D.tokenizer.separator) || o == r) {
+                    if (0 < s) {
+                        var c = D.utils.clone(t) || {};
+                        c.position = [a, s], c.index = i.length, i.push(new D.Token(n.slice(a, o), c))
+                    }
+                    a = o + 1
+                }
+            }
+            return i
+        }, D.tokenizer.separator = /[\s\-]+/, D.Pipeline = function() {
+            this._stack = []
+        }, D.Pipeline.registeredFunctions = Object.create(null), D.Pipeline.registerFunction = function(e, t) {
+            t in this.registeredFunctions && D.utils.warn("Overwriting existing registered function: " + t), e.label = t, D.Pipeline.registeredFunctions[e.label] = e
+        }, D.Pipeline.warnIfFunctionNotRegistered = function(e) {
+            e.label && e.label in this.registeredFunctions || D.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n", e)
+        }, D.Pipeline.load = function(e) {
+            var n = new D.Pipeline;
+            return e.forEach(function(e) {
+                var t = D.Pipeline.registeredFunctions[e];
+                if (!t) throw new Error("Cannot load unregistered function: " + e);
+                n.add(t)
+            }), n
+        }, D.Pipeline.prototype.add = function() {
+            Array.prototype.slice.call(arguments).forEach(function(e) {
+                D.Pipeline.warnIfFunctionNotRegistered(e), this._stack.push(e)
+            }, this)
+        }, D.Pipeline.prototype.after = function(e, t) {
+            D.Pipeline.warnIfFunctionNotRegistered(t);
+            var n = this._stack.indexOf(e);
+            if (-1 == n) throw new Error("Cannot find existingFn");
+            n += 1, this._stack.splice(n, 0, t)
+        }, D.Pipeline.prototype.before = function(e, t) {
+            D.Pipeline.warnIfFunctionNotRegistered(t);
+            var n = this._stack.indexOf(e);
+            if (-1 == n) throw new Error("Cannot find existingFn");
+            this._stack.splice(n, 0, t)
+        }, D.Pipeline.prototype.remove = function(e) {
+            var t = this._stack.indexOf(e); - 1 != t && this._stack.splice(t, 1)
+        }, D.Pipeline.prototype.run = function(e) {
+            for (var t = this._stack.length, n = 0; n < t; n++) {
+                for (var r = this._stack[n], i = [], o = 0; o < e.length; o++) {
+                    var a = r(e[o], o, e);
+                    if (void 0 !== a && "" !== a)
+                        if (Array.isArray(a))
+                            for (var s = 0; s < a.length; s++) i.push(a[s]);
+                        else i.push(a)
+                }
+                e = i
+            }
+            return e
+        }, D.Pipeline.prototype.runString = function(e, t) {
+            var n = new D.Token(e, t);
+            return this.run([n]).map(function(e) {
+                return e.toString()
+            })
+        }, D.Pipeline.prototype.reset = function() {
+            this._stack = []
+        }, D.Pipeline.prototype.toJSON = function() {
+            return this._stack.map(function(e) {
+                return D.Pipeline.warnIfFunctionNotRegistered(e), e.label
+            })
+        }, D.Vector = function(e) {
+            this._magnitude = 0, this.elements = e || []
+        }, D.Vector.prototype.positionForIndex = function(e) {
+            if (0 == this.elements.length) return 0;
+            for (var t = 0, n = this.elements.length / 2, r = n - t, i = Math.floor(r / 2), o = this.elements[2 * i]; 1 < r && (o < e && (t = i), e < o && (n = i), o != e);) r = n - t, i = t + Math.floor(r / 2), o = this.elements[2 * i];
+            return o == e ? 2 * i : e < o ? 2 * i : o < e ? 2 * (i + 1) : void 0
+        }, D.Vector.prototype.insert = function(e, t) {
+            this.upsert(e, t, function() {
+                throw "duplicate index"
+            })
+        }, D.Vector.prototype.upsert = function(e, t, n) {
+            this._magnitude = 0;
+            var r = this.positionForIndex(e);
+            this.elements[r] == e ? this.elements[r + 1] = n(this.elements[r + 1], t) : this.elements.splice(r, 0, e, t)
+        }, D.Vector.prototype.magnitude = function() {
+            if (this._magnitude) return this._magnitude;
+            for (var e = 0, t = this.elements.length, n = 1; n < t; n += 2) {
+                var r = this.elements[n];
+                e += r * r
+            }
+            return this._magnitude = Math.sqrt(e)
+        }, D.Vector.prototype.dot = function(e) {
+            for (var t = 0, n = this.elements, r = e.elements, i = n.length, o = r.length, a = 0, s = 0, c = 0, l = 0; c < i && l < o;)(a = n[c]) < (s = r[l]) ? c += 2 : s < a ? l += 2 : a == s && (t += n[c + 1] * r[l + 1], c += 2, l += 2);
+            return t
+        }, D.Vector.prototype.similarity = function(e) {
+            return this.dot(e) / this.magnitude() || 0
+        }, D.Vector.prototype.toArray = function() {
+            for (var e = new Array(this.elements.length / 2), t = 1, n = 0; t < this.elements.length; t += 2, n++) e[n] = this.elements[t];
+            return e
+        }, D.Vector.prototype.toJSON = function() {
+            return this.elements
+        }, D.stemmer = (l = {
+            ational: "ate",
+            tional: "tion",
+            enci: "ence",
+            anci: "ance",
+            izer: "ize",
+            bli: "ble",
+            alli: "al",
+            entli: "ent",
+            eli: "e",
+            ousli: "ous",
+            ization: "ize",
+            ation: "ate",
+            ator: "ate",
+            alism: "al",
+            iveness: "ive",
+            fulness: "ful",
+            ousness: "ous",
+            aliti: "al",
+            iviti: "ive",
+            biliti: "ble",
+            logi: "log"
+        }, u = {
+            icate: "ic",
+            ative: "",
+            alize: "al",
+            iciti: "ic",
+            ical: "ic",
+            ful: "",
+            ness: ""
+        }, e = "[aeiouy]", n = "[^aeiou][^aeiouy]*", f = new RegExp("^([^aeiou][^aeiouy]*)?[aeiouy][aeiou]*[^aeiou][^aeiouy]*"), d = new RegExp("^([^aeiou][^aeiouy]*)?[aeiouy][aeiou]*[^aeiou][^aeiouy]*[aeiouy][aeiou]*[^aeiou][^aeiouy]*"), h = new RegExp("^([^aeiou][^aeiouy]*)?[aeiouy][aeiou]*[^aeiou][^aeiouy]*([aeiouy][aeiou]*)?$"), p = new RegExp("^([^aeiou][^aeiouy]*)?[aeiouy]"), m = /^(.+?)(ss|i)es$/, y = /^(.+?)([^s])s$/, v = /^(.+?)eed$/, g = /^(.+?)(ed|ing)$/, w = /.$/, _ = /(at|bl|iz)$/, E = new RegExp("([^aeiouylsz])\\1$"), x = new RegExp("^" + n + e + "[^aeiouwxy]$"), b = /^(.+?[^aeiou])y$/, k = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/, S = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/, T = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/, L = /^(.+?)(s|t)(ion)$/, R = /^(.+?)e$/, O = /ll$/, C = new RegExp("^" + n + e + "[^aeiouwxy]$"), r = function(e) {
+            var t, n, r, i, o, a, s;
+            if (e.length < 3) return e;
+            if ("y" == (r = e.substr(0, 1)) && (e = r.toUpperCase() + e.substr(1)), o = y, (i = m).test(e) ? e = e.replace(i, "$1$2") : o.test(e) && (e = e.replace(o, "$1$2")), o = g, (i = v).test(e)) {
+                var c = i.exec(e);
+                (i = f).test(c[1]) && (i = w, e = e.replace(i, ""))
+            } else if (o.test(e)) {
+                t = (c = o.exec(e))[1], (o = p).test(t) && (a = E, s = x, (o = _).test(e = t) ? e += "e" : a.test(e) ? (i = w, e = e.replace(i, "")) : s.test(e) && (e += "e"))
+            }(i = b).test(e) && (e = (t = (c = i.exec(e))[1]) + "i");
+            (i = k).test(e) && (t = (c = i.exec(e))[1], n = c[2], (i = f).test(t) && (e = t + l[n]));
+            (i = S).test(e) && (t = (c = i.exec(e))[1], n = c[2], (i = f).test(t) && (e = t + u[n]));
+            if (o = L, (i = T).test(e)) t = (c = i.exec(e))[1], (i = d).test(t) && (e = t);
+            else if (o.test(e)) {
+                t = (c = o.exec(e))[1] + c[2], (o = d).test(t) && (e = t)
+            }(i = R).test(e) && (t = (c = i.exec(e))[1], o = h, a = C, ((i = d).test(t) || o.test(t) && !a.test(t)) && (e = t));
+            return o = d, (i = O).test(e) && o.test(e) && (i = w, e = e.replace(i, "")), "y" == r && (e = r.toLowerCase() + e.substr(1)), e
+        }, function(e) {
+            return e.update(r)
+        }), D.Pipeline.registerFunction(D.stemmer, "stemmer"), D.generateStopWordFilter = function(e) {
+            var t = e.reduce(function(e, t) {
+                return e[t] = t, e
+            }, {});
+            return function(e) {
+                if (e && t[e.toString()] !== e.toString()) return e
+            }
+        }, D.stopWordFilter = D.generateStopWordFilter(["a", "able", "about", "across", "after", "all", "almost", "also", "am", "among", "an", "and", "any", "are", "as", "at", "be", "because", "been", "but", "by", "can", "cannot", "could", "dear", "did", "do", "does", "either", "else", "ever", "every", "for", "from", "get", "got", "had", "has", "have", "he", "her", "hers", "him", "his", "how", "however", "i", "if", "in", "into", "is", "it", "its", "just", "least", "let", "like", "likely", "may", "me", "might", "most", "must", "my", "neither", "no", "nor", "not", "of", "off", "often", "on", "only", "or", "other", "our", "own", "rather", "said", "say", "says", "she", "should", "since", "so", "some", "than", "that", "the", "their", "them", "then", "there", "these", "they", "this", "tis", "to", "too", "twas", "us", "wants", "was", "we", "were", "what", "when", "where", "which", "while", "who", "whom", "why", "will", "with", "would", "yet", "you", "your"]), D.Pipeline.registerFunction(D.stopWordFilter, "stopWordFilter"), D.trimmer = function(e) {
+            return e.update(function(e) {
+                return e.replace(/^\W+/, "").replace(/\W+$/, "")
+            })
+        }, D.Pipeline.registerFunction(D.trimmer, "trimmer"), D.TokenSet = function() {
+            this.final = !1, this.edges = {}, this.id = D.TokenSet._nextId, D.TokenSet._nextId += 1
+        }, D.TokenSet._nextId = 1, D.TokenSet.fromArray = function(e) {
+            for (var t = new D.TokenSet.Builder, n = 0, r = e.length; n < r; n++) t.insert(e[n]);
+            return t.finish(), t.root
+        }, D.TokenSet.fromClause = function(e) {
+            return "editDistance" in e ? D.TokenSet.fromFuzzyString(e.term, e.editDistance) : D.TokenSet.fromString(e.term)
+        }, D.TokenSet.fromFuzzyString = function(e, t) {
+            for (var n = new D.TokenSet, r = [{
+                    node: n,
+                    editsRemaining: t,
+                    str: e
+                }]; r.length;) {
+                var i = r.pop();
+                if (0 < i.str.length) {
+                    var o, a = i.str.charAt(0);
+                    a in i.node.edges ? o = i.node.edges[a] : (o = new D.TokenSet, i.node.edges[a] = o), 1 == i.str.length && (o.final = !0), r.push({
+                        node: o,
+                        editsRemaining: i.editsRemaining,
+                        str: i.str.slice(1)
+                    })
+                }
+                if (0 != i.editsRemaining) {
+                    if ("*" in i.node.edges) var s = i.node.edges["*"];
+                    else {
+                        s = new D.TokenSet;
+                        i.node.edges["*"] = s
+                    }
+                    if (0 == i.str.length && (s.final = !0), r.push({
+                            node: s,
+                            editsRemaining: i.editsRemaining - 1,
+                            str: i.str
+                        }), 1 < i.str.length && r.push({
+                            node: i.node,
+                            editsRemaining: i.editsRemaining - 1,
+                            str: i.str.slice(1)
+                        }), 1 == i.str.length && (i.node.final = !0), 1 <= i.str.length) {
+                        if ("*" in i.node.edges) var c = i.node.edges["*"];
+                        else {
+                            c = new D.TokenSet;
+                            i.node.edges["*"] = c
+                        }
+                        1 == i.str.length && (c.final = !0), r.push({
+                            node: c,
+                            editsRemaining: i.editsRemaining - 1,
+                            str: i.str.slice(1)
+                        })
+                    }
+                    if (1 < i.str.length) {
+                        var l, u = i.str.charAt(0),
+                            f = i.str.charAt(1);
+                        f in i.node.edges ? l = i.node.edges[f] : (l = new D.TokenSet, i.node.edges[f] = l), 1 == i.str.length && (l.final = !0), r.push({
+                            node: l,
+                            editsRemaining: i.editsRemaining - 1,
+                            str: u + i.str.slice(2)
+                        })
+                    }
+                }
+            }
+            return n
+        }, D.TokenSet.fromString = function(e) {
+            for (var t = new D.TokenSet, n = t, r = 0, i = e.length; r < i; r++) {
+                var o = e[r],
+                    a = r == i - 1;
+                if ("*" == o)(t.edges[o] = t).final = a;
+                else {
+                    var s = new D.TokenSet;
+                    s.final = a, t.edges[o] = s, t = s
+                }
+            }
+            return n
+        }, D.TokenSet.prototype.toArray = function() {
+            for (var e = [], t = [{
+                    prefix: "",
+                    node: this
+                }]; t.length;) {
+                var n = t.pop(),
+                    r = Object.keys(n.node.edges),
+                    i = r.length;
+                n.node.final && (n.prefix.charAt(0), e.push(n.prefix));
+                for (var o = 0; o < i; o++) {
+                    var a = r[o];
+                    t.push({
+                        prefix: n.prefix.concat(a),
+                        node: n.node.edges[a]
+                    })
+                }
+            }
+            return e
+        }, D.TokenSet.prototype.toString = function() {
+            if (this._str) return this._str;
+            for (var e = this.final ? "1" : "0", t = Object.keys(this.edges).sort(), n = t.length, r = 0; r < n; r++) {
+                var i = t[r];
+                e = e + i + this.edges[i].id
+            }
+            return e
+        }, D.TokenSet.prototype.intersect = function(e) {
+            for (var t = new D.TokenSet, n = void 0, r = [{
+                    qNode: e,
+                    output: t,
+                    node: this
+                }]; r.length;) {
+                n = r.pop();
+                for (var i = Object.keys(n.qNode.edges), o = i.length, a = Object.keys(n.node.edges), s = a.length, c = 0; c < o; c++)
+                    for (var l = i[c], u = 0; u < s; u++) {
+                        var f = a[u];
+                        if (f == l || "*" == l) {
+                            var d = n.node.edges[f],
+                                h = n.qNode.edges[l],
+                                p = d.final && h.final,
+                                m = void 0;
+                            f in n.output.edges ? (m = n.output.edges[f]).final = m.final || p : ((m = new D.TokenSet).final = p, n.output.edges[f] = m), r.push({
+                                qNode: h,
+                                output: m,
+                                node: d
+                            })
+                        }
+                    }
+            }
+            return t
+        }, D.TokenSet.Builder = function() {
+            this.previousWord = "", this.root = new D.TokenSet, this.uncheckedNodes = [], this.minimizedNodes = {}
+        }, D.TokenSet.Builder.prototype.insert = function(e) {
+            var t, n = 0;
+            if (e < this.previousWord) throw new Error("Out of order word insertion");
+            for (var r = 0; r < e.length && r < this.previousWord.length && e[r] == this.previousWord[r]; r++) n++;
+            this.minimize(n), t = 0 == this.uncheckedNodes.length ? this.root : this.uncheckedNodes[this.uncheckedNodes.length - 1].child;
+            for (r = n; r < e.length; r++) {
+                var i = new D.TokenSet,
+                    o = e[r];
+                t.edges[o] = i, this.uncheckedNodes.push({
+                    parent: t,
+                    char: o,
+                    child: i
+                }), t = i
+            }
+            t.final = !0, this.previousWord = e
+        }, D.TokenSet.Builder.prototype.finish = function() {
+            this.minimize(0)
+        }, D.TokenSet.Builder.prototype.minimize = function(e) {
+            for (var t = this.uncheckedNodes.length - 1; e <= t; t--) {
+                var n = this.uncheckedNodes[t],
+                    r = n.child.toString();
+                r in this.minimizedNodes ? n.parent.edges[n.char] = this.minimizedNodes[r] : (n.child._str = r, this.minimizedNodes[r] = n.child), this.uncheckedNodes.pop()
+            }
+        }, D.Index = function(e) {
+            this.invertedIndex = e.invertedIndex, this.fieldVectors = e.fieldVectors, this.tokenSet = e.tokenSet, this.fields = e.fields, this.pipeline = e.pipeline
+        }, D.Index.prototype.search = function(t) {
+            return this.query(function(e) {
+                new D.QueryParser(t, e).parse()
+            })
+        }, D.Index.prototype.query = function(e) {
+            for (var t = new D.Query(this.fields), n = Object.create(null), r = Object.create(null), i = Object.create(null), o = Object.create(null), a = Object.create(null), s = 0; s < this.fields.length; s++) r[this.fields[s]] = new D.Vector;
+            e.call(t, t);
+            for (s = 0; s < t.clauses.length; s++) {
+                var c = t.clauses[s],
+                    l = null,
+                    u = D.Set.complete;
+                l = c.usePipeline ? this.pipeline.runString(c.term, {
+                    fields: c.fields
+                }) : [c.term];
+                for (var f = 0; f < l.length; f++) {
+                    var d = l[f];
+                    c.term = d;
+                    var h = D.TokenSet.fromClause(c),
+                        p = this.tokenSet.intersect(h).toArray();
+                    if (0 === p.length && c.presence === D.Query.presence.REQUIRED) {
+                        for (var m = 0; m < c.fields.length; m++) {
+                            o[Q = c.fields[m]] = D.Set.empty
+                        }
+                        break
+                    }
+                    for (var y = 0; y < p.length; y++) {
+                        var v = p[y],
+                            g = this.invertedIndex[v],
+                            w = g._index;
+                        for (m = 0; m < c.fields.length; m++) {
+                            var _ = g[Q = c.fields[m]],
+                                E = Object.keys(_),
+                                x = v + "/" + Q,
+                                b = new D.Set(E);
+                            if (c.presence == D.Query.presence.REQUIRED && (u = u.union(b), void 0 === o[Q] && (o[Q] = D.Set.complete)), c.presence != D.Query.presence.PROHIBITED) {
+                                if (r[Q].upsert(w, c.boost, function(e, t) {
+                                        return e + t
+                                    }), !i[x]) {
+                                    for (var k = 0; k < E.length; k++) {
+                                        var S, T = E[k],
+                                            L = new D.FieldRef(T, Q),
+                                            R = _[T];
+                                        void 0 === (S = n[L]) ? n[L] = new D.MatchData(v, Q, R) : S.add(v, Q, R)
+                                    }
+                                    i[x] = !0
+                                }
+                            } else void 0 === a[Q] && (a[Q] = D.Set.empty), a[Q] = a[Q].union(b)
+                        }
+                    }
+                }
+                if (c.presence === D.Query.presence.REQUIRED)
+                    for (m = 0; m < c.fields.length; m++) {
+                        o[Q = c.fields[m]] = o[Q].intersect(u)
+                    }
+            }
+            var O = D.Set.complete,
+                C = D.Set.empty;
+            for (s = 0; s < this.fields.length; s++) {
+                var Q;
+                o[Q = this.fields[s]] && (O = O.intersect(o[Q])), a[Q] && (C = C.union(a[Q]))
+            }
+            var P = Object.keys(n),
+                A = [],
+                I = Object.create(null);
+            if (t.isNegated()) {
+                P = Object.keys(this.fieldVectors);
+                for (s = 0; s < P.length; s++) {
+                    L = P[s];
+                    var M = D.FieldRef.fromString(L);
+                    n[L] = new D.MatchData
+                }
+            }
+            for (s = 0; s < P.length; s++) {
+                var N = (M = D.FieldRef.fromString(P[s])).docRef;
+                if (O.contains(N) && !C.contains(N)) {
+                    var j, F = this.fieldVectors[M],
+                        H = r[M.fieldName].similarity(F);
+                    if (void 0 !== (j = I[N])) j.score += H, j.matchData.combine(n[M]);
+                    else {
+                        var q = {
+                            ref: N,
+                            score: H,
+                            matchData: n[M]
+                        };
+                        I[N] = q, A.push(q)
+                    }
+                }
+            }
+            return A.sort(function(e, t) {
+                return t.score - e.score
+            })
+        }, D.Index.prototype.toJSON = function() {
+            var e = Object.keys(this.invertedIndex).sort().map(function(e) {
+                    return [e, this.invertedIndex[e]]
+                }, this),
+                t = Object.keys(this.fieldVectors).map(function(e) {
+                    return [e, this.fieldVectors[e].toJSON()]
+                }, this);
+            return {
+                version: D.version,
+                fields: this.fields,
+                fieldVectors: t,
+                invertedIndex: e,
+                pipeline: this.pipeline.toJSON()
+            }
+        }, D.Index.load = function(e) {
+            var t = {},
+                n = {},
+                r = e.fieldVectors,
+                i = Object.create(null),
+                o = e.invertedIndex,
+                a = new D.TokenSet.Builder,
+                s = D.Pipeline.load(e.pipeline);
+            e.version != D.version && D.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + D.version + "' does not match serialized index '" + e.version + "'");
+            for (var c = 0; c < r.length; c++) {
+                var l = (f = r[c])[0],
+                    u = f[1];
+                n[l] = new D.Vector(u)
+            }
+            for (c = 0; c < o.length; c++) {
+                var f, d = (f = o[c])[0],
+                    h = f[1];
+                a.insert(d), i[d] = h
+            }
+            return a.finish(), t.fields = e.fields, t.fieldVectors = n, t.invertedIndex = i, t.tokenSet = a.root, t.pipeline = s, new D.Index(t)
+        }, D.Builder = function() {
+            this._ref = "id", this._fields = Object.create(null), this._documents = Object.create(null), this.invertedIndex = Object.create(null), this.fieldTermFrequencies = {}, this.fieldLengths = {}, this.tokenizer = D.tokenizer, this.pipeline = new D.Pipeline, this.searchPipeline = new D.Pipeline, this.documentCount = 0, this._b = .75, this._k1 = 1.2, this.termIndex = 0, this.metadataWhitelist = []
+        }, D.Builder.prototype.ref = function(e) {
+            this._ref = e
+        }, D.Builder.prototype.field = function(e, t) {
+            if (/\//.test(e)) throw new RangeError("Field '" + e + "' contains illegal character '/'");
+            this._fields[e] = t || {}
+        }, D.Builder.prototype.b = function(e) {
+            this._b = e < 0 ? 0 : 1 < e ? 1 : e
+        }, D.Builder.prototype.k1 = function(e) {
+            this._k1 = e
+        }, D.Builder.prototype.add = function(e, t) {
+            var n = e[this._ref],
+                r = Object.keys(this._fields);
+            this._documents[n] = t || {}, this.documentCount += 1;
+            for (var i = 0; i < r.length; i++) {
+                var o = r[i],
+                    a = this._fields[o].extractor,
+                    s = a ? a(e) : e[o],
+                    c = this.tokenizer(s, {
+                        fields: [o]
+                    }),
+                    l = this.pipeline.run(c),
+                    u = new D.FieldRef(n, o),
+                    f = Object.create(null);
+                this.fieldTermFrequencies[u] = f, this.fieldLengths[u] = 0, this.fieldLengths[u] += l.length;
+                for (var d = 0; d < l.length; d++) {
+                    var h = l[d];
+                    if (null == f[h] && (f[h] = 0), f[h] += 1, null == this.invertedIndex[h]) {
+                        var p = Object.create(null);
+                        p._index = this.termIndex, this.termIndex += 1;
+                        for (var m = 0; m < r.length; m++) p[r[m]] = Object.create(null);
+                        this.invertedIndex[h] = p
+                    }
+                    null == this.invertedIndex[h][o][n] && (this.invertedIndex[h][o][n] = Object.create(null));
+                    for (var y = 0; y < this.metadataWhitelist.length; y++) {
+                        var v = this.metadataWhitelist[y],
+                            g = h.metadata[v];
+                        null == this.invertedIndex[h][o][n][v] && (this.invertedIndex[h][o][n][v] = []), this.invertedIndex[h][o][n][v].push(g)
+                    }
+                }
+            }
+        }, D.Builder.prototype.calculateAverageFieldLengths = function() {
+            for (var e = Object.keys(this.fieldLengths), t = e.length, n = {}, r = {}, i = 0; i < t; i++) {
+                var o = D.FieldRef.fromString(e[i]),
+                    a = o.fieldName;
+                r[a] || (r[a] = 0), r[a] += 1, n[a] || (n[a] = 0), n[a] += this.fieldLengths[o]
+            }
+            var s = Object.keys(this._fields);
+            for (i = 0; i < s.length; i++) {
+                var c = s[i];
+                n[c] = n[c] / r[c]
+            }
+            this.averageFieldLength = n
+        }, D.Builder.prototype.createFieldVectors = function() {
+            for (var e = {}, t = Object.keys(this.fieldTermFrequencies), n = t.length, r = Object.create(null), i = 0; i < n; i++) {
+                for (var o = D.FieldRef.fromString(t[i]), a = o.fieldName, s = this.fieldLengths[o], c = new D.Vector, l = this.fieldTermFrequencies[o], u = Object.keys(l), f = u.length, d = this._fields[a].boost || 1, h = this._documents[o.docRef].boost || 1, p = 0; p < f; p++) {
+                    var m, y, v, g = u[p],
+                        w = l[g],
+                        _ = this.invertedIndex[g]._index;
+                    void 0 === r[g] ? (m = D.idf(this.invertedIndex[g], this.documentCount), r[g] = m) : m = r[g], y = m * ((this._k1 + 1) * w) / (this._k1 * (1 - this._b + this._b * (s / this.averageFieldLength[a])) + w), y *= d, y *= h, v = Math.round(1e3 * y) / 1e3, c.insert(_, v)
+                }
+                e[o] = c
+            }
+            this.fieldVectors = e
+        }, D.Builder.prototype.createTokenSet = function() {
+            this.tokenSet = D.TokenSet.fromArray(Object.keys(this.invertedIndex).sort())
+        }, D.Builder.prototype.build = function() {
+            return this.calculateAverageFieldLengths(), this.createFieldVectors(), this.createTokenSet(), new D.Index({
+                invertedIndex: this.invertedIndex,
+                fieldVectors: this.fieldVectors,
+                tokenSet: this.tokenSet,
+                fields: Object.keys(this._fields),
+                pipeline: this.searchPipeline
+            })
+        }, D.Builder.prototype.use = function(e) {
+            var t = Array.prototype.slice.call(arguments, 1);
+            t.unshift(this), e.apply(this, t)
+        }, D.MatchData = function(e, t, n) {
+            for (var r = Object.create(null), i = Object.keys(n || {}), o = 0; o < i.length; o++) {
+                var a = i[o];
+                r[a] = n[a].slice()
+            }
+            this.metadata = Object.create(null), void 0 !== e && (this.metadata[e] = Object.create(null), this.metadata[e][t] = r)
+        }, D.MatchData.prototype.combine = function(e) {
+            for (var t = Object.keys(e.metadata), n = 0; n < t.length; n++) {
+                var r = t[n],
+                    i = Object.keys(e.metadata[r]);
+                null == this.metadata[r] && (this.metadata[r] = Object.create(null));
+                for (var o = 0; o < i.length; o++) {
+                    var a = i[o],
+                        s = Object.keys(e.metadata[r][a]);
+                    null == this.metadata[r][a] && (this.metadata[r][a] = Object.create(null));
+                    for (var c = 0; c < s.length; c++) {
+                        var l = s[c];
+                        null == this.metadata[r][a][l] ? this.metadata[r][a][l] = e.metadata[r][a][l] : this.metadata[r][a][l] = this.metadata[r][a][l].concat(e.metadata[r][a][l])
+                    }
+                }
+            }
+        }, D.MatchData.prototype.add = function(e, t, n) {
+            if (!(e in this.metadata)) return this.metadata[e] = Object.create(null), void(this.metadata[e][t] = n);
+            if (t in this.metadata[e])
+                for (var r = Object.keys(n), i = 0; i < r.length; i++) {
+                    var o = r[i];
+                    o in this.metadata[e][t] ? this.metadata[e][t][o] = this.metadata[e][t][o].concat(n[o]) : this.metadata[e][t][o] = n[o]
+                } else this.metadata[e][t] = n
+        }, D.Query = function(e) {
+            this.clauses = [], this.allFields = e
+        }, D.Query.wildcard = new String("*"), D.Query.wildcard.NONE = 0, D.Query.wildcard.LEADING = 1, D.Query.wildcard.TRAILING = 2, D.Query.presence = {
+            OPTIONAL: 1,
+            REQUIRED: 2,
+            PROHIBITED: 3
+        }, D.Query.prototype.clause = function(e) {
+            return "fields" in e || (e.fields = this.allFields), "boost" in e || (e.boost = 1), "usePipeline" in e || (e.usePipeline = !0), "wildcard" in e || (e.wildcard = D.Query.wildcard.NONE), e.wildcard & D.Query.wildcard.LEADING && e.term.charAt(0) != D.Query.wildcard && (e.term = "*" + e.term), e.wildcard & D.Query.wildcard.TRAILING && e.term.slice(-1) != D.Query.wildcard && (e.term = e.term + "*"), "presence" in e || (e.presence = D.Query.presence.OPTIONAL), this.clauses.push(e), this
+        }, D.Query.prototype.isNegated = function() {
+            for (var e = 0; e < this.clauses.length; e++)
+                if (this.clauses[e].presence != D.Query.presence.PROHIBITED) return !1;
+            return !0
+        }, D.Query.prototype.term = function(e, t) {
+            if (Array.isArray(e)) return e.forEach(function(e) {
+                this.term(e, D.utils.clone(t))
+            }, this), this;
+            var n = t || {};
+            return n.term = e.toString(), this.clause(n), this
+        }, D.QueryParseError = function(e, t, n) {
+            this.name = "QueryParseError", this.message = e, this.start = t, this.end = n
+        }, D.QueryParseError.prototype = new Error, D.QueryLexer = function(e) {
+            this.lexemes = [], this.str = e, this.length = e.length, this.pos = 0, this.start = 0, this.escapeCharPositions = []
+        }, D.QueryLexer.prototype.run = function() {
+            for (var e = D.QueryLexer.lexText; e;) e = e(this)
+        }, D.QueryLexer.prototype.sliceString = function() {
+            for (var e = [], t = this.start, n = this.pos, r = 0; r < this.escapeCharPositions.length; r++) n = this.escapeCharPositions[r], e.push(this.str.slice(t, n)), t = n + 1;
+            return e.push(this.str.slice(t, this.pos)), this.escapeCharPositions.length = 0, e.join("")
+        }, D.QueryLexer.prototype.emit = function(e) {
+            this.lexemes.push({
+                type: e,
+                str: this.sliceString(),
+                start: this.start,
+                end: this.pos
+            }), this.start = this.pos
+        }, D.QueryLexer.prototype.escapeCharacter = function() {
+            this.escapeCharPositions.push(this.pos - 1), this.pos += 1
+        }, D.QueryLexer.prototype.next = function() {
+            if (this.pos >= this.length) return D.QueryLexer.EOS;
+            var e = this.str.charAt(this.pos);
+            return this.pos += 1, e
+        }, D.QueryLexer.prototype.width = function() {
+            return this.pos - this.start
+        }, D.QueryLexer.prototype.ignore = function() {
+            this.start == this.pos && (this.pos += 1), this.start = this.pos
+        }, D.QueryLexer.prototype.backup = function() {
+            this.pos -= 1
+        }, D.QueryLexer.prototype.acceptDigitRun = function() {
+            for (var e, t; 47 < (t = (e = this.next()).charCodeAt(0)) && t < 58;);
+            e != D.QueryLexer.EOS && this.backup()
+        }, D.QueryLexer.prototype.more = function() {
+            return this.pos < this.length
+        }, D.QueryLexer.EOS = "EOS", D.QueryLexer.FIELD = "FIELD", D.QueryLexer.TERM = "TERM", D.QueryLexer.EDIT_DISTANCE = "EDIT_DISTANCE", D.QueryLexer.BOOST = "BOOST", D.QueryLexer.PRESENCE = "PRESENCE", D.QueryLexer.lexField = function(e) {
+            return e.backup(), e.emit(D.QueryLexer.FIELD), e.ignore(), D.QueryLexer.lexText
+        }, D.QueryLexer.lexTerm = function(e) {
+            if (1 < e.width() && (e.backup(), e.emit(D.QueryLexer.TERM)), e.ignore(), e.more()) return D.QueryLexer.lexText
+        }, D.QueryLexer.lexEditDistance = function(e) {
+            return e.ignore(), e.acceptDigitRun(), e.emit(D.QueryLexer.EDIT_DISTANCE), D.QueryLexer.lexText
+        }, D.QueryLexer.lexBoost = function(e) {
+            return e.ignore(), e.acceptDigitRun(), e.emit(D.QueryLexer.BOOST), D.QueryLexer.lexText
+        }, D.QueryLexer.lexEOS = function(e) {
+            0 < e.width() && e.emit(D.QueryLexer.TERM)
+        }, D.QueryLexer.termSeparator = D.tokenizer.separator, D.QueryLexer.lexText = function(e) {
+            for (;;) {
+                var t = e.next();
+                if (t == D.QueryLexer.EOS) return D.QueryLexer.lexEOS;
+                if (92 != t.charCodeAt(0)) {
+                    if (":" == t) return D.QueryLexer.lexField;
+                    if ("~" == t) return e.backup(), 0 < e.width() && e.emit(D.QueryLexer.TERM), D.QueryLexer.lexEditDistance;
+                    if ("^" == t) return e.backup(), 0 < e.width() && e.emit(D.QueryLexer.TERM), D.QueryLexer.lexBoost;
+                    if ("+" == t && 1 === e.width()) return e.emit(D.QueryLexer.PRESENCE), D.QueryLexer.lexText;
+                    if ("-" == t && 1 === e.width()) return e.emit(D.QueryLexer.PRESENCE), D.QueryLexer.lexText;
+                    if (t.match(D.QueryLexer.termSeparator)) return D.QueryLexer.lexTerm
+                } else e.escapeCharacter()
+            }
+        }, D.QueryParser = function(e, t) {
+            this.lexer = new D.QueryLexer(e), this.query = t, this.currentClause = {}, this.lexemeIdx = 0
+        }, D.QueryParser.prototype.parse = function() {
+            this.lexer.run(), this.lexemes = this.lexer.lexemes;
+            for (var e = D.QueryParser.parseClause; e;) e = e(this);
+            return this.query
+        }, D.QueryParser.prototype.peekLexeme = function() {
+            return this.lexemes[this.lexemeIdx]
+        }, D.QueryParser.prototype.consumeLexeme = function() {
+            var e = this.peekLexeme();
+            return this.lexemeIdx += 1, e
+        }, D.QueryParser.prototype.nextClause = function() {
+            var e = this.currentClause;
+            this.query.clause(e), this.currentClause = {}
+        }, D.QueryParser.parseClause = function(e) {
+            var t = e.peekLexeme();
+            if (null != t) switch (t.type) {
+                case D.QueryLexer.PRESENCE:
+                    return D.QueryParser.parsePresence;
+                case D.QueryLexer.FIELD:
+                    return D.QueryParser.parseField;
+                case D.QueryLexer.TERM:
+                    return D.QueryParser.parseTerm;
+                default:
+                    var n = "expected either a field or a term, found " + t.type;
+                    throw 1 <= t.str.length && (n += " with value '" + t.str + "'"), new D.QueryParseError(n, t.start, t.end)
+            }
+        }, D.QueryParser.parsePresence = function(e) {
+            var t = e.consumeLexeme();
+            if (null != t) {
+                switch (t.str) {
+                    case "-":
+                        e.currentClause.presence = D.Query.presence.PROHIBITED;
+                        break;
+                    case "+":
+                        e.currentClause.presence = D.Query.presence.REQUIRED;
+                        break;
+                    default:
+                        var n = "unrecognised presence operator'" + t.str + "'";
+                        throw new D.QueryParseError(n, t.start, t.end)
+                }
+                var r = e.peekLexeme();
+                if (null == r) {
+                    n = "expecting term or field, found nothing";
+                    throw new D.QueryParseError(n, t.start, t.end)
+                }
+                switch (r.type) {
+                    case D.QueryLexer.FIELD:
+                        return D.QueryParser.parseField;
+                    case D.QueryLexer.TERM:
+                        return D.QueryParser.parseTerm;
+                    default:
+                        n = "expecting term or field, found '" + r.type + "'";
+                        throw new D.QueryParseError(n, r.start, r.end)
+                }
+            }
+        }, D.QueryParser.parseField = function(e) {
+            var t = e.consumeLexeme();
+            if (null != t) {
+                if (-1 == e.query.allFields.indexOf(t.str)) {
+                    var n = e.query.allFields.map(function(e) {
+                            return "'" + e + "'"
+                        }).join(", "),
+                        r = "unrecognised field '" + t.str + "', possible fields: " + n;
+                    throw new D.QueryParseError(r, t.start, t.end)
+                }
+                e.currentClause.fields = [t.str];
+                var i = e.peekLexeme();
+                if (null == i) {
+                    r = "expecting term, found nothing";
+                    throw new D.QueryParseError(r, t.start, t.end)
+                }
+                switch (i.type) {
+                    case D.QueryLexer.TERM:
+                        return D.QueryParser.parseTerm;
+                    default:
+                        r = "expecting term, found '" + i.type + "'";
+                        throw new D.QueryParseError(r, i.start, i.end)
+                }
+            }
+        }, D.QueryParser.parseTerm = function(e) {
+            var t = e.consumeLexeme();
+            if (null != t) {
+                e.currentClause.term = t.str.toLowerCase(), -1 != t.str.indexOf("*") && (e.currentClause.usePipeline = !1);
+                var n = e.peekLexeme();
+                if (null != n) switch (n.type) {
+                    case D.QueryLexer.TERM:
+                        return e.nextClause(), D.QueryParser.parseTerm;
+                    case D.QueryLexer.FIELD:
+                        return e.nextClause(), D.QueryParser.parseField;
+                    case D.QueryLexer.EDIT_DISTANCE:
+                        return D.QueryParser.parseEditDistance;
+                    case D.QueryLexer.BOOST:
+                        return D.QueryParser.parseBoost;
+                    case D.QueryLexer.PRESENCE:
+                        return e.nextClause(), D.QueryParser.parsePresence;
+                    default:
+                        var r = "Unexpected lexeme type '" + n.type + "'";
+                        throw new D.QueryParseError(r, n.start, n.end)
+                } else e.nextClause()
+            }
+        }, D.QueryParser.parseEditDistance = function(e) {
+            var t = e.consumeLexeme();
+            if (null != t) {
+                var n = parseInt(t.str, 10);
+                if (isNaN(n)) {
+                    var r = "edit distance must be numeric";
+                    throw new D.QueryParseError(r, t.start, t.end)
+                }
+                e.currentClause.editDistance = n;
+                var i = e.peekLexeme();
+                if (null != i) switch (i.type) {
+                    case D.QueryLexer.TERM:
+                        return e.nextClause(), D.QueryParser.parseTerm;
+                    case D.QueryLexer.FIELD:
+                        return e.nextClause(), D.QueryParser.parseField;
+                    case D.QueryLexer.EDIT_DISTANCE:
+                        return D.QueryParser.parseEditDistance;
+                    case D.QueryLexer.BOOST:
+                        return D.QueryParser.parseBoost;
+                    case D.QueryLexer.PRESENCE:
+                        return e.nextClause(), D.QueryParser.parsePresence;
+                    default:
+                        r = "Unexpected lexeme type '" + i.type + "'";
+                        throw new D.QueryParseError(r, i.start, i.end)
+                } else e.nextClause()
+            }
+        }, D.QueryParser.parseBoost = function(e) {
+            var t = e.consumeLexeme();
+            if (null != t) {
+                var n = parseInt(t.str, 10);
+                if (isNaN(n)) {
+                    var r = "boost must be numeric";
+                    throw new D.QueryParseError(r, t.start, t.end)
+                }
+                e.currentClause.boost = n;
+                var i = e.peekLexeme();
+                if (null != i) switch (i.type) {
+                    case D.QueryLexer.TERM:
+                        return e.nextClause(), D.QueryParser.parseTerm;
+                    case D.QueryLexer.FIELD:
+                        return e.nextClause(), D.QueryParser.parseField;
+                    case D.QueryLexer.EDIT_DISTANCE:
+                        return D.QueryParser.parseEditDistance;
+                    case D.QueryLexer.BOOST:
+                        return D.QueryParser.parseBoost;
+                    case D.QueryLexer.PRESENCE:
+                        return e.nextClause(), D.QueryParser.parsePresence;
+                    default:
+                        r = "Unexpected lexeme type '" + i.type + "'";
+                        throw new D.QueryParseError(r, i.start, i.end)
+                } else e.nextClause()
+            }
+        }, void 0 === (c = "function" == typeof(s = function() {
+            return D
+        }) ? s.call(o, a, o, i) : s) || (i.exports = c)
+    }()
+}]));

File diff suppressed because it is too large
+ 0 - 0
docs/_build/html/_static/javascripts/lunr/lunr.da.js


File diff suppressed because it is too large
+ 0 - 0
docs/_build/html/_static/javascripts/lunr/lunr.de.js


File diff suppressed because it is too large
+ 0 - 0
docs/_build/html/_static/javascripts/lunr/lunr.du.js


File diff suppressed because it is too large
+ 0 - 0
docs/_build/html/_static/javascripts/lunr/lunr.es.js


File diff suppressed because it is too large
+ 0 - 0
docs/_build/html/_static/javascripts/lunr/lunr.fi.js


Some files were not shown because too many files changed in this diff