瀏覽代碼

Merge branch 'BlackDex-update-deps'

Daniel García 2 年之前
父節點
當前提交
226da67bc0

+ 103 - 102
Cargo.lock

@@ -54,9 +54,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.19"
+version = "0.7.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
 dependencies = [
  "memchr",
 ]
@@ -122,9 +122,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.58"
+version = "0.1.59"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
+checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -173,7 +173,7 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide",
+ "miniz_oxide 0.5.4",
  "object",
  "rustc-demangle",
 ]
@@ -240,9 +240,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
 
 [[package]]
 name = "cached"
@@ -283,9 +283,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
 
 [[package]]
 name = "cc"
-version = "1.0.75"
+version = "1.0.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41ca34107f97baef6cfb231b32f36115781856b8f8208e8c580e0bcaea374842"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
 
 [[package]]
 name = "cfg-if"
@@ -295,9 +295,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
 dependencies = [
  "iana-time-zone",
  "num-integer",
@@ -308,9 +308,9 @@ dependencies = [
 
 [[package]]
 name = "chrono-tz"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a87b30366b6766751277791b473b674f3bf7fb75696841c784a3eb7e7fbf44ee"
+checksum = "fa48fa079165080f11d7753fd0bc175b7d391f276b965fe4b55bfad67856e463"
 dependencies = [
  "chrono",
  "chrono-tz-build",
@@ -445,9 +445,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.12"
+version = "0.8.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
 dependencies = [
  "cfg-if",
 ]
@@ -484,9 +484,9 @@ dependencies = [
 
 [[package]]
 name = "cxx"
-version = "1.0.81"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888"
+checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
 dependencies = [
  "cc",
  "cxxbridge-flags",
@@ -496,9 +496,9 @@ dependencies = [
 
 [[package]]
 name = "cxx-build"
-version = "1.0.81"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3"
+checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
 dependencies = [
  "cc",
  "codespan-reporting",
@@ -511,15 +511,15 @@ dependencies = [
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.81"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f"
+checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.81"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704"
+checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -663,9 +663,9 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
  "block-buffer",
  "crypto-common",
@@ -714,9 +714,9 @@ dependencies = [
 
 [[package]]
 name = "enum-as-inner"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
+checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -768,12 +768,12 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.24"
+version = "1.0.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
 dependencies = [
  "crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
 ]
 
 [[package]]
@@ -959,9 +959,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
 [[package]]
 name = "governor"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de1b4626e87b9eb1d603ed23067ba1e29ec1d0b35325a2b96c3fe1cf20871f56"
+checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5"
 dependencies = [
  "cfg-if",
  "dashmap",
@@ -1198,9 +1198,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.9.1"
+version = "1.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -1233,14 +1233,14 @@ dependencies = [
 
 [[package]]
 name = "ipconfig"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98"
+checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
 dependencies = [
  "socket2",
  "widestring",
  "winapi",
- "winreg 0.7.0",
+ "winreg",
 ]
 
 [[package]]
@@ -1283,9 +1283,9 @@ dependencies = [
 
 [[package]]
 name = "jsonwebtoken"
-version = "8.1.1"
+version = "8.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c"
+checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828"
 dependencies = [
  "base64",
  "pem",
@@ -1331,17 +1331,18 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.137"
+version = "0.2.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
 
 [[package]]
 name = "libmimalloc-sys"
-version = "0.1.27"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c37567b180c1af25924b303ddf1ee4467653783440c62360beb2b322a4d93361"
+checksum = "04d1c67deb83e6b75fa4fe3309e09cfeade12e7721d95322af500d3814ea60c9"
 dependencies = [
  "cc",
+ "libc",
 ]
 
 [[package]]
@@ -1472,9 +1473,9 @@ dependencies = [
 
 [[package]]
 name = "mimalloc"
-version = "0.1.31"
+version = "0.1.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b32d6a9ac92d0239d7bfa31137fb47634ac7272a3c11bcee91379ac100781670"
+checksum = "9b2374e2999959a7b583e1811a1ddbf1d3a4b9496eceb9746f1192a59d871eca"
 dependencies = [
  "libmimalloc-sys",
 ]
@@ -1500,6 +1501,15 @@ dependencies = [
  "adler",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
 [[package]]
 name = "mio"
 version = "0.8.5"
@@ -1561,9 +1571,9 @@ dependencies = [
 
 [[package]]
 name = "nix"
-version = "0.25.0"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
 dependencies = [
  "autocfg",
  "bitflags",
@@ -1686,9 +1696,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "openssl"
-version = "0.10.42"
+version = "0.10.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
+checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376"
 dependencies = [
  "bitflags",
  "cfg-if",
@@ -1727,9 +1737,9 @@ dependencies = [
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.77"
+version = "0.9.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a"
+checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
 dependencies = [
  "autocfg",
  "cc",
@@ -1757,9 +1767,9 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
+checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1823,9 +1833,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
 [[package]]
 name = "pest"
-version = "2.4.1"
+version = "2.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8"
+checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
 dependencies = [
  "thiserror",
  "ucd-trie",
@@ -1833,9 +1843,9 @@ dependencies = [
 
 [[package]]
 name = "pest_derive"
-version = "2.4.1"
+version = "2.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f"
+checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344"
 dependencies = [
  "pest",
  "pest_generator",
@@ -1843,9 +1853,9 @@ dependencies = [
 
 [[package]]
 name = "pest_generator"
-version = "2.4.1"
+version = "2.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4"
+checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c"
 dependencies = [
  "pest",
  "pest_meta",
@@ -1856,9 +1866,9 @@ dependencies = [
 
 [[package]]
 name = "pest_meta"
-version = "2.4.1"
+version = "2.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe"
+checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20"
 dependencies = [
  "once_cell",
  "pest",
@@ -2055,9 +2065,9 @@ dependencies = [
 
 [[package]]
 name = "quoted_printable"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
+checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
 
 [[package]]
 name = "r2d2"
@@ -2175,9 +2185,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.12"
+version = "0.11.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
+checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
 dependencies = [
  "async-compression",
  "base64",
@@ -2214,7 +2224,7 @@ dependencies = [
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
- "winreg 0.10.1",
+ "winreg",
 ]
 
 [[package]]
@@ -2474,9 +2484,9 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
 
 [[package]]
 name = "serde"
-version = "1.0.147"
+version = "1.0.148"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
 dependencies = [
  "serde_derive",
 ]
@@ -2493,9 +2503,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.147"
+version = "1.0.148"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2504,9 +2514,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.87"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
 dependencies = [
  "itoa",
  "ryu",
@@ -2527,9 +2537,9 @@ dependencies = [
 
 [[package]]
 name = "sha-1"
-version = "0.10.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -2663,9 +2673,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
 [[package]]
 name = "syn"
-version = "1.0.103"
+version = "1.0.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2792,9 +2802,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.21.2"
+version = "1.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
+checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
 dependencies = [
  "autocfg",
  "bytes",
@@ -2812,9 +2822,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "1.8.0"
+version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2867,9 +2877,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-tungstenite"
-version = "0.17.2"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
+checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
 dependencies = [
  "futures-util",
  "log",
@@ -2983,9 +2993,9 @@ dependencies = [
 
 [[package]]
 name = "trust-dns-proto"
-version = "0.21.2"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d"
+checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
 dependencies = [
  "async-trait",
  "cfg-if",
@@ -2997,32 +3007,32 @@ dependencies = [
  "idna 0.2.3",
  "ipnet",
  "lazy_static",
- "log",
  "rand",
  "smallvec",
  "thiserror",
  "tinyvec",
  "tokio",
+ "tracing",
  "url",
 ]
 
 [[package]]
 name = "trust-dns-resolver"
-version = "0.21.2"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558"
+checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
 dependencies = [
  "cfg-if",
  "futures-util",
  "ipconfig",
  "lazy_static",
- "log",
  "lru-cache",
  "parking_lot",
  "resolv-conf",
  "smallvec",
  "thiserror",
  "tokio",
+ "tracing",
  "trust-dns-proto",
 ]
 
@@ -3034,9 +3044,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
 [[package]]
 name = "tungstenite"
-version = "0.17.3"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
+checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
 dependencies = [
  "base64",
  "byteorder",
@@ -3045,7 +3055,7 @@ dependencies = [
  "httparse",
  "log",
  "rand",
- "sha-1",
+ "sha1",
  "thiserror",
  "url",
  "utf-8",
@@ -3151,9 +3161,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
 [[package]]
 name = "uuid"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
+checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
 dependencies = [
  "getrandom",
 ]
@@ -3552,15 +3562,6 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
-[[package]]
-name = "winreg"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
-dependencies = [
- "winapi",
-]
-
 [[package]]
 name = "winreg"
 version = "0.10.1"

+ 12 - 12
Cargo.toml

@@ -55,17 +55,17 @@ num-derive = "0.3.3"
 rocket = { version = "0.5.0-rc.2", features = ["tls", "json"], default-features = false }
 
 # WebSockets libraries
-tokio-tungstenite = "0.17.2"
+tokio-tungstenite = "0.18.0"
 rmpv = "1.0.0" # MessagePack library
 dashmap = "5.4.0"
 
 # Async futures
 futures = "0.3.25"
-tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time"] }
+tokio = { version = "1.22.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time"] }
 
 # A generic serialization/deserialization framework
-serde = { version = "1.0.147", features = ["derive"] }
-serde_json = "1.0.87"
+serde = { version = "1.0.148", features = ["derive"] }
+serde_json = "1.0.89"
 
 # A safe, extensible ORM and Query builder
 diesel = { version = "2.0.2", features = ["chrono", "r2d2"] }
@@ -79,11 +79,11 @@ rand = { version = "0.8.5", features = ["small_rng"] }
 ring = "0.16.20"
 
 # UUID generation
-uuid = { version = "1.2.1", features = ["v4"] }
+uuid = { version = "1.2.2", features = ["v4"] }
 
 # Date and time libraries
-chrono = { version = "0.4.22", features = ["clock", "serde"], default-features = false }
-chrono-tz = "0.8.0"
+chrono = { version = "0.4.23", features = ["clock", "serde"], default-features = false }
+chrono-tz = "0.8.1"
 time = "0.3.17"
 
 # Job scheduler
@@ -116,13 +116,13 @@ email_address = "0.2.4"
 handlebars = { version = "4.3.5", features = ["dir_source"] }
 
 # HTTP client
-reqwest = { version = "0.11.12", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
+reqwest = { version = "0.11.13", features = ["stream", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
 
 # For favicon extraction from main website
 html5gum = "0.5.2"
 regex = { version = "1.7.0", features = ["std", "perf", "unicode-perl"], default-features = false }
 data-url = "0.2.0"
-bytes = "1.2.1"
+bytes = "1.3.0"
 cached = "0.40.0"
 
 # Used for custom short lived cookie jar during favicon extraction
@@ -130,14 +130,14 @@ cookie = "0.16.1"
 cookie_store = "0.19.0"
 
 # Used by U2F, JWT and Postgres
-openssl = "0.10.42"
+openssl = "0.10.43"
 
 # CLI argument parsing
 pico-args = "0.5.0"
 
 # Macro ident concatenation
 paste = "1.0.9"
-governor = "0.5.0"
+governor = "0.5.1"
 
 # Capture CTRL+C
 ctrlc = { version = "3.2.3", features = ["termination"] }
@@ -147,7 +147,7 @@ semver = "1.0.14"
 
 # Allow overriding the default memory allocator
 # Mainly used for the musl builds, since the default musl malloc is very slow
-mimalloc = { version = "0.1.31", features = ["secure"], default-features = false, optional = true }
+mimalloc = { version = "0.1.32", features = ["secure"], default-features = false, optional = true }
 
 [patch.crates-io]
 # Using a patched version of multer-rs (Used by Rocket) to fix attachment/send file uploads

+ 1 - 1
src/api/web.rs

@@ -98,7 +98,7 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
         "vaultwarden-icon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-icon.png"))),
         "bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
         "bootstrap-native.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
-        "identicon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
+        "jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
         "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
         "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
         "jquery-3.6.1.slim.js" => {

+ 85 - 114
src/static/scripts/bootstrap.css

@@ -1,6 +1,6 @@
 @charset "UTF-8";
 /*!
- * Bootstrap  v5.2.0 (https://getbootstrap.com/)
+ * Bootstrap  v5.2.3 (https://getbootstrap.com/)
  * Copyright 2011-2022 The Bootstrap Authors
  * Copyright 2011-2022 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@@ -2468,6 +2468,7 @@ textarea.form-control-lg {
   height: 100%;
   padding: 1rem 0.75rem;
   overflow: hidden;
+  text-align: start;
   text-overflow: ellipsis;
   white-space: nowrap;
   pointer-events: none;
@@ -2547,14 +2548,14 @@ textarea.form-control-lg {
 .input-group > .form-control:focus,
 .input-group > .form-select:focus,
 .input-group > .form-floating:focus-within {
-  z-index: 3;
+  z-index: 5;
 }
 .input-group .btn {
   position: relative;
   z-index: 2;
 }
 .input-group .btn:focus {
-  z-index: 3;
+  z-index: 5;
 }
 
 .input-group-text {
@@ -2609,10 +2610,13 @@ textarea.form-control-lg {
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
-.input-group > :not(:first-child):not(.dropdown-menu):not(.form-floating):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback),
+.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
+  margin-left: -1px;
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
 .input-group > .form-floating:not(:first-child) > .form-control,
 .input-group > .form-floating:not(:first-child) > .form-select {
-  margin-left: -1px;
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
 }
@@ -2699,14 +2703,11 @@ textarea.form-control-lg {
   margin-left: 0.5em;
 }
 
-.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,
-.was-validated .input-group .form-select:valid,
-.input-group .form-select.is-valid {
-  z-index: 1;
-}
-.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus,
-.was-validated .input-group .form-select:valid:focus,
-.input-group .form-select.is-valid:focus {
+.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid,
+.was-validated .input-group > .form-select:not(:focus):valid,
+.input-group > .form-select:not(:focus).is-valid,
+.was-validated .input-group > .form-floating:not(:focus-within):valid,
+.input-group > .form-floating:not(:focus-within).is-valid {
   z-index: 3;
 }
 
@@ -2792,15 +2793,12 @@ textarea.form-control-lg {
   margin-left: 0.5em;
 }
 
-.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,
-.was-validated .input-group .form-select:invalid,
-.input-group .form-select.is-invalid {
-  z-index: 2;
-}
-.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus,
-.was-validated .input-group .form-select:invalid:focus,
-.input-group .form-select.is-invalid:focus {
-  z-index: 3;
+.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid,
+.was-validated .input-group > .form-select:not(:focus):invalid,
+.input-group > .form-select:not(:focus).is-invalid,
+.was-validated .input-group > .form-floating:not(:focus-within):invalid,
+.input-group > .form-floating:not(:focus-within).is-invalid {
+  z-index: 4;
 }
 
 .btn {
@@ -2815,6 +2813,7 @@ textarea.form-control-lg {
   --bs-btn-border-width: 1px;
   --bs-btn-border-color: transparent;
   --bs-btn-border-radius: 0.375rem;
+  --bs-btn-hover-border-color: transparent;
   --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
   --bs-btn-disabled-opacity: 0.65;
   --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);
@@ -2847,19 +2846,29 @@ textarea.form-control-lg {
   background-color: var(--bs-btn-hover-bg);
   border-color: var(--bs-btn-hover-border-color);
 }
-.btn-check:focus + .btn, .btn:focus {
+.btn-check + .btn:hover {
+  color: var(--bs-btn-color);
+  background-color: var(--bs-btn-bg);
+  border-color: var(--bs-btn-border-color);
+}
+.btn:focus-visible {
   color: var(--bs-btn-hover-color);
   background-color: var(--bs-btn-hover-bg);
   border-color: var(--bs-btn-hover-border-color);
   outline: 0;
   box-shadow: var(--bs-btn-focus-box-shadow);
 }
-.btn-check:checked + .btn, .btn-check:active + .btn, .btn:active, .btn.active, .btn.show {
+.btn-check:focus-visible + .btn {
+  border-color: var(--bs-btn-hover-border-color);
+  outline: 0;
+  box-shadow: var(--bs-btn-focus-box-shadow);
+}
+.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {
   color: var(--bs-btn-active-color);
   background-color: var(--bs-btn-active-bg);
   border-color: var(--bs-btn-active-border-color);
 }
-.btn-check:checked + .btn:focus, .btn-check:active + .btn:focus, .btn:active:focus, .btn.active:focus, .btn.show:focus {
+.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible {
   box-shadow: var(--bs-btn-focus-box-shadow);
 }
 .btn:disabled, .btn.disabled, fieldset:disabled .btn {
@@ -3157,7 +3166,7 @@ textarea.form-control-lg {
   --bs-btn-focus-shadow-rgb: 49, 132, 253;
   text-decoration: underline;
 }
-.btn-link:focus {
+.btn-link:focus-visible {
   color: var(--bs-btn-color);
 }
 .btn-link:hover {
@@ -3242,6 +3251,7 @@ textarea.form-control-lg {
 }
 
 .dropdown-menu {
+  --bs-dropdown-zindex: 1000;
   --bs-dropdown-min-width: 10rem;
   --bs-dropdown-padding-x: 0;
   --bs-dropdown-padding-y: 0.5rem;
@@ -3268,7 +3278,7 @@ textarea.form-control-lg {
   --bs-dropdown-header-padding-x: 1rem;
   --bs-dropdown-header-padding-y: 0.5rem;
   position: absolute;
-  z-index: 1000;
+  z-index: var(--bs-dropdown-zindex);
   display: none;
   min-width: var(--bs-dropdown-min-width);
   padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
@@ -3568,7 +3578,7 @@ textarea.form-control-lg {
 .btn-group {
   border-radius: 0.375rem;
 }
-.btn-group > .btn:not(:first-child),
+.btn-group > :not(.btn-check:first-child) + .btn,
 .btn-group > .btn-group:not(:first-child) {
   margin-left: -1px;
 }
@@ -3678,7 +3688,7 @@ textarea.form-control-lg {
   border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color);
 }
 .nav-tabs .nav-link {
-  margin-bottom: calc(var(--bs-nav-tabs-border-width) * -1);
+  margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));
   background: none;
   border: var(--bs-nav-tabs-border-width) solid transparent;
   border-top-left-radius: var(--bs-nav-tabs-border-radius);
@@ -3700,7 +3710,7 @@ textarea.form-control-lg {
   border-color: var(--bs-nav-tabs-link-active-border-color);
 }
 .nav-tabs .dropdown-menu {
-  margin-top: calc(var(--bs-nav-tabs-border-width) * -1);
+  margin-top: calc(-1 * var(--bs-nav-tabs-border-width));
   border-top-left-radius: 0;
   border-top-right-radius: 0;
 }
@@ -4357,7 +4367,7 @@ textarea.form-control-lg {
 }
 
 .accordion {
-  --bs-accordion-color: #000;
+  --bs-accordion-color: #212529;
   --bs-accordion-bg: #fff;
   --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
   --bs-accordion-border-color: var(--bs-border-color);
@@ -4366,9 +4376,9 @@ textarea.form-control-lg {
   --bs-accordion-inner-border-radius: calc(0.375rem - 1px);
   --bs-accordion-btn-padding-x: 1.25rem;
   --bs-accordion-btn-padding-y: 1rem;
-  --bs-accordion-btn-color: var(--bs-body-color);
+  --bs-accordion-btn-color: #212529;
   --bs-accordion-btn-bg: var(--bs-accordion-bg);
-  --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+  --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
   --bs-accordion-btn-icon-width: 1.25rem;
   --bs-accordion-btn-icon-transform: rotate(-180deg);
   --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
@@ -4404,7 +4414,7 @@ textarea.form-control-lg {
 .accordion-button:not(.collapsed) {
   color: var(--bs-accordion-active-color);
   background-color: var(--bs-accordion-active-bg);
-  box-shadow: inset 0 calc(var(--bs-accordion-border-width) * -1) 0 var(--bs-accordion-border-color);
+  box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color);
 }
 .accordion-button:not(.collapsed)::after {
   background-image: var(--bs-accordion-btn-active-icon);
@@ -4487,7 +4497,7 @@ textarea.form-control-lg {
 .accordion-flush .accordion-item:last-child {
   border-bottom: 0;
 }
-.accordion-flush .accordion-item .accordion-button {
+.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed {
   border-radius: 0;
 }
 
@@ -4753,12 +4763,6 @@ textarea.form-control-lg {
   color: #101214;
 }
 
-@-webkit-keyframes progress-bar-stripes {
-  0% {
-    background-position-x: 1rem;
-  }
-}
-
 @keyframes progress-bar-stripes {
   0% {
     background-position-x: 1rem;
@@ -4804,12 +4808,10 @@ textarea.form-control-lg {
 }
 
 .progress-bar-animated {
-  -webkit-animation: 1s linear infinite progress-bar-stripes;
   animation: 1s linear infinite progress-bar-stripes;
 }
 @media (prefers-reduced-motion: reduce) {
   .progress-bar-animated {
-    -webkit-animation: none;
     animation: none;
   }
 }
@@ -4896,18 +4898,18 @@ textarea.form-control-lg {
   border-top-width: 0;
 }
 .list-group-item + .list-group-item.active {
-  margin-top: calc(var(--bs-list-group-border-width) * -1);
+  margin-top: calc(-1 * var(--bs-list-group-border-width));
   border-top-width: var(--bs-list-group-border-width);
 }
 
 .list-group-horizontal {
   flex-direction: row;
 }
-.list-group-horizontal > .list-group-item:first-child {
+.list-group-horizontal > .list-group-item:first-child:not(:last-child) {
   border-bottom-left-radius: var(--bs-list-group-border-radius);
   border-top-right-radius: 0;
 }
-.list-group-horizontal > .list-group-item:last-child {
+.list-group-horizontal > .list-group-item:last-child:not(:first-child) {
   border-top-right-radius: var(--bs-list-group-border-radius);
   border-bottom-left-radius: 0;
 }
@@ -4919,7 +4921,7 @@ textarea.form-control-lg {
   border-left-width: 0;
 }
 .list-group-horizontal > .list-group-item + .list-group-item.active {
-  margin-left: calc(var(--bs-list-group-border-width) * -1);
+  margin-left: calc(-1 * var(--bs-list-group-border-width));
   border-left-width: var(--bs-list-group-border-width);
 }
 
@@ -4927,11 +4929,11 @@ textarea.form-control-lg {
   .list-group-horizontal-sm {
     flex-direction: row;
   }
-  .list-group-horizontal-sm > .list-group-item:first-child {
+  .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {
     border-bottom-left-radius: var(--bs-list-group-border-radius);
     border-top-right-radius: 0;
   }
-  .list-group-horizontal-sm > .list-group-item:last-child {
+  .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {
     border-top-right-radius: var(--bs-list-group-border-radius);
     border-bottom-left-radius: 0;
   }
@@ -4943,7 +4945,7 @@ textarea.form-control-lg {
     border-left-width: 0;
   }
   .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
-    margin-left: calc(var(--bs-list-group-border-width) * -1);
+    margin-left: calc(-1 * var(--bs-list-group-border-width));
     border-left-width: var(--bs-list-group-border-width);
   }
 }
@@ -4951,11 +4953,11 @@ textarea.form-control-lg {
   .list-group-horizontal-md {
     flex-direction: row;
   }
-  .list-group-horizontal-md > .list-group-item:first-child {
+  .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {
     border-bottom-left-radius: var(--bs-list-group-border-radius);
     border-top-right-radius: 0;
   }
-  .list-group-horizontal-md > .list-group-item:last-child {
+  .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {
     border-top-right-radius: var(--bs-list-group-border-radius);
     border-bottom-left-radius: 0;
   }
@@ -4967,7 +4969,7 @@ textarea.form-control-lg {
     border-left-width: 0;
   }
   .list-group-horizontal-md > .list-group-item + .list-group-item.active {
-    margin-left: calc(var(--bs-list-group-border-width) * -1);
+    margin-left: calc(-1 * var(--bs-list-group-border-width));
     border-left-width: var(--bs-list-group-border-width);
   }
 }
@@ -4975,11 +4977,11 @@ textarea.form-control-lg {
   .list-group-horizontal-lg {
     flex-direction: row;
   }
-  .list-group-horizontal-lg > .list-group-item:first-child {
+  .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {
     border-bottom-left-radius: var(--bs-list-group-border-radius);
     border-top-right-radius: 0;
   }
-  .list-group-horizontal-lg > .list-group-item:last-child {
+  .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {
     border-top-right-radius: var(--bs-list-group-border-radius);
     border-bottom-left-radius: 0;
   }
@@ -4991,7 +4993,7 @@ textarea.form-control-lg {
     border-left-width: 0;
   }
   .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
-    margin-left: calc(var(--bs-list-group-border-width) * -1);
+    margin-left: calc(-1 * var(--bs-list-group-border-width));
     border-left-width: var(--bs-list-group-border-width);
   }
 }
@@ -4999,11 +5001,11 @@ textarea.form-control-lg {
   .list-group-horizontal-xl {
     flex-direction: row;
   }
-  .list-group-horizontal-xl > .list-group-item:first-child {
+  .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {
     border-bottom-left-radius: var(--bs-list-group-border-radius);
     border-top-right-radius: 0;
   }
-  .list-group-horizontal-xl > .list-group-item:last-child {
+  .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {
     border-top-right-radius: var(--bs-list-group-border-radius);
     border-bottom-left-radius: 0;
   }
@@ -5015,7 +5017,7 @@ textarea.form-control-lg {
     border-left-width: 0;
   }
   .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
-    margin-left: calc(var(--bs-list-group-border-width) * -1);
+    margin-left: calc(-1 * var(--bs-list-group-border-width));
     border-left-width: var(--bs-list-group-border-width);
   }
 }
@@ -5023,11 +5025,11 @@ textarea.form-control-lg {
   .list-group-horizontal-xxl {
     flex-direction: row;
   }
-  .list-group-horizontal-xxl > .list-group-item:first-child {
+  .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {
     border-bottom-left-radius: var(--bs-list-group-border-radius);
     border-top-right-radius: 0;
   }
-  .list-group-horizontal-xxl > .list-group-item:last-child {
+  .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {
     border-top-right-radius: var(--bs-list-group-border-radius);
     border-bottom-left-radius: 0;
   }
@@ -5039,7 +5041,7 @@ textarea.form-control-lg {
     border-left-width: 0;
   }
   .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {
-    margin-left: calc(var(--bs-list-group-border-width) * -1);
+    margin-left: calc(-1 * var(--bs-list-group-border-width));
     border-left-width: var(--bs-list-group-border-width);
   }
 }
@@ -5199,6 +5201,7 @@ textarea.form-control-lg {
 }
 
 .toast {
+  --bs-toast-zindex: 1090;
   --bs-toast-padding-x: 0.75rem;
   --bs-toast-padding-y: 0.5rem;
   --bs-toast-spacing: 1.5rem;
@@ -5232,8 +5235,9 @@ textarea.form-control-lg {
 }
 
 .toast-container {
+  --bs-toast-zindex: 1090;
   position: absolute;
-  z-index: 1090;
+  z-index: var(--bs-toast-zindex);
   width: -webkit-max-content;
   width: -moz-max-content;
   width: max-content;
@@ -5256,7 +5260,7 @@ textarea.form-control-lg {
   border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));
 }
 .toast-header .btn-close {
-  margin-right: calc(var(--bs-toast-padding-x) * -0.5);
+  margin-right: calc(-0.5 * var(--bs-toast-padding-x));
   margin-left: var(--bs-toast-padding-x);
 }
 
@@ -5383,7 +5387,7 @@ textarea.form-control-lg {
 }
 .modal-header .btn-close {
   padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);
-  margin: calc(var(--bs-modal-header-padding-y) * -0.5) calc(var(--bs-modal-header-padding-x) * -0.5) calc(var(--bs-modal-header-padding-y) * -0.5) auto;
+  margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;
 }
 
 .modal-title {
@@ -5673,7 +5677,7 @@ textarea.form-control-lg {
   --bs-popover-header-padding-x: 1rem;
   --bs-popover-header-padding-y: 0.5rem;
   --bs-popover-header-font-size: 1rem;
-  --bs-popover-header-color: var(--bs-heading-color);
+  --bs-popover-header-color: ;
   --bs-popover-header-bg: #f0f0f0;
   --bs-popover-body-padding-x: 1rem;
   --bs-popover-body-padding-y: 1rem;
@@ -5720,7 +5724,7 @@ textarea.form-control-lg {
 }
 
 .bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {
-  bottom: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));
+  bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));
 }
 .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {
   border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;
@@ -5736,7 +5740,7 @@ textarea.form-control-lg {
 
 /* rtl:begin:ignore */
 .bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {
-  left: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));
+  left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));
   width: var(--bs-popover-arrow-height);
   height: var(--bs-popover-arrow-width);
 }
@@ -5754,7 +5758,7 @@ textarea.form-control-lg {
 
 /* rtl:end:ignore */
 .bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {
-  top: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));
+  top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));
 }
 .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {
   border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);
@@ -5773,14 +5777,14 @@ textarea.form-control-lg {
   left: 50%;
   display: block;
   width: var(--bs-popover-arrow-width);
-  margin-left: calc(var(--bs-popover-arrow-width) * -0.5);
+  margin-left: calc(-0.5 * var(--bs-popover-arrow-width));
   content: "";
   border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg);
 }
 
 /* rtl:begin:ignore */
 .bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {
-  right: calc(var(--bs-popover-arrow-height) * -1 - var(--bs-popover-border-width));
+  right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));
   width: var(--bs-popover-arrow-height);
   height: var(--bs-popover-arrow-width);
 }
@@ -5857,7 +5861,6 @@ textarea.form-control-lg {
   display: block;
 }
 
-/* rtl:begin:ignore */
 .carousel-item-next:not(.carousel-item-start),
 .active.carousel-item-end {
   transform: translateX(100%);
@@ -5868,7 +5871,6 @@ textarea.form-control-lg {
   transform: translateX(-100%);
 }
 
-/* rtl:end:ignore */
 .carousel-fade .carousel-item {
   opacity: 0;
   transition-property: opacity;
@@ -6030,16 +6032,9 @@ textarea.form-control-lg {
   height: var(--bs-spinner-height);
   vertical-align: var(--bs-spinner-vertical-align);
   border-radius: 50%;
-  -webkit-animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);
   animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);
 }
 
-@-webkit-keyframes spinner-border {
-  to {
-    transform: rotate(360deg) /* rtl:ignore */;
-  }
-}
-
 @keyframes spinner-border {
   to {
     transform: rotate(360deg) /* rtl:ignore */;
@@ -6062,16 +6057,6 @@ textarea.form-control-lg {
   --bs-spinner-border-width: 0.2em;
 }
 
-@-webkit-keyframes spinner-grow {
-  0% {
-    transform: scale(0);
-  }
-  50% {
-    opacity: 1;
-    transform: none;
-  }
-}
-
 @keyframes spinner-grow {
   0% {
     transform: scale(0);
@@ -6103,6 +6088,7 @@ textarea.form-control-lg {
   }
 }
 .offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {
+  --bs-offcanvas-zindex: 1045;
   --bs-offcanvas-width: 400px;
   --bs-offcanvas-height: 30vh;
   --bs-offcanvas-padding-x: 1rem;
@@ -6118,7 +6104,7 @@ textarea.form-control-lg {
   .offcanvas-sm {
     position: fixed;
     bottom: 0;
-    z-index: 1045;
+    z-index: var(--bs-offcanvas-zindex);
     display: flex;
     flex-direction: column;
     max-width: 100%;
@@ -6206,7 +6192,7 @@ textarea.form-control-lg {
   .offcanvas-md {
     position: fixed;
     bottom: 0;
-    z-index: 1045;
+    z-index: var(--bs-offcanvas-zindex);
     display: flex;
     flex-direction: column;
     max-width: 100%;
@@ -6294,7 +6280,7 @@ textarea.form-control-lg {
   .offcanvas-lg {
     position: fixed;
     bottom: 0;
-    z-index: 1045;
+    z-index: var(--bs-offcanvas-zindex);
     display: flex;
     flex-direction: column;
     max-width: 100%;
@@ -6382,7 +6368,7 @@ textarea.form-control-lg {
   .offcanvas-xl {
     position: fixed;
     bottom: 0;
-    z-index: 1045;
+    z-index: var(--bs-offcanvas-zindex);
     display: flex;
     flex-direction: column;
     max-width: 100%;
@@ -6470,7 +6456,7 @@ textarea.form-control-lg {
   .offcanvas-xxl {
     position: fixed;
     bottom: 0;
-    z-index: 1045;
+    z-index: var(--bs-offcanvas-zindex);
     display: flex;
     flex-direction: column;
     max-width: 100%;
@@ -6557,7 +6543,7 @@ textarea.form-control-lg {
 .offcanvas {
   position: fixed;
   bottom: 0;
-  z-index: 1045;
+  z-index: var(--bs-offcanvas-zindex);
   display: flex;
   flex-direction: column;
   max-width: 100%;
@@ -6635,9 +6621,9 @@ textarea.form-control-lg {
 }
 .offcanvas-header .btn-close {
   padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);
-  margin-top: calc(var(--bs-offcanvas-padding-y) * -0.5);
-  margin-right: calc(var(--bs-offcanvas-padding-x) * -0.5);
-  margin-bottom: calc(var(--bs-offcanvas-padding-y) * -0.5);
+  margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
+  margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));
+  margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
 }
 
 .offcanvas-title {
@@ -6677,16 +6663,9 @@ textarea.form-control-lg {
 }
 
 .placeholder-glow .placeholder {
-  -webkit-animation: placeholder-glow 2s ease-in-out infinite;
   animation: placeholder-glow 2s ease-in-out infinite;
 }
 
-@-webkit-keyframes placeholder-glow {
-  50% {
-    opacity: 0.2;
-  }
-}
-
 @keyframes placeholder-glow {
   50% {
     opacity: 0.2;
@@ -6697,17 +6676,9 @@ textarea.form-control-lg {
   mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);
   -webkit-mask-size: 200% 100%;
   mask-size: 200% 100%;
-  -webkit-animation: placeholder-wave 2s linear infinite;
   animation: placeholder-wave 2s linear infinite;
 }
 
-@-webkit-keyframes placeholder-wave {
-  100% {
-    -webkit-mask-position: -200% 0%;
-    mask-position: -200% 0%;
-  }
-}
-
 @keyframes placeholder-wave {
   100% {
     -webkit-mask-position: -200% 0%;

+ 11 - 5
src/static/scripts/datatables.css

@@ -4,10 +4,10 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs5/dt-1.12.1
+ *   https://datatables.net/download/#bs5/dt-1.13.1
  *
  * Included libraries:
- *   DataTables 1.12.1
+ *   DataTables 1.13.1
  */
 
 @charset "UTF-8";
@@ -63,7 +63,7 @@ table.dataTable thead > tr > td.sorting_desc_disabled:after {
   opacity: 0.125;
   right: 10px;
   line-height: 9px;
-  font-size: 0.9em;
+  font-size: 0.8em;
 }
 table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before,
 table.dataTable thead > tr > td.sorting:before,
@@ -72,7 +72,7 @@ table.dataTable thead > tr > td.sorting_desc:before,
 table.dataTable thead > tr > td.sorting_asc_disabled:before,
 table.dataTable thead > tr > td.sorting_desc_disabled:before {
   bottom: 50%;
-  content: "";
+  content: "";
 }
 table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
 table.dataTable thead > tr > td.sorting:after,
@@ -81,7 +81,7 @@ table.dataTable thead > tr > td.sorting_desc:after,
 table.dataTable thead > tr > td.sorting_asc_disabled:after,
 table.dataTable thead > tr > td.sorting_desc_disabled:after {
   top: 50%;
-  content: "";
+  content: "";
 }
 table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
 table.dataTable thead > tr > td.sorting_asc:before,
@@ -287,6 +287,9 @@ table.dataTable > tbody > tr.selected > * {
   box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9);
   color: white;
 }
+table.dataTable > tbody > tr.selected a {
+  color: #090a0b;
+}
 table.dataTable.table-striped > tbody > tr.odd > * {
   box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
 }
@@ -335,6 +338,9 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination {
   white-space: nowrap;
   justify-content: flex-end;
 }
+div.dataTables_wrapper div.dt-row {
+  position: relative;
+}
 
 div.dataTables_scrollHead table.dataTable {
   margin-bottom: 0 !important;

+ 79 - 38
src/static/scripts/datatables.js

@@ -4,20 +4,20 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs5/dt-1.12.1
+ *   https://datatables.net/download/#bs5/dt-1.13.1
  *
  * Included libraries:
- *   DataTables 1.12.1
+ *   DataTables 1.13.1
  */
 
-/*! DataTables 1.12.1
+/*! DataTables 1.13.1
  * ©2008-2022 SpryMedia Ltd - datatables.net/license
  */
 
 /**
  * @summary     DataTables
  * @description Paginate, search and order HTML tables
- * @version     1.12.1
+ * @version     1.13.1
  * @author      SpryMedia Ltd
  * @contact     www.datatables.net
  * @copyright   SpryMedia Ltd.
@@ -1162,6 +1162,10 @@
 				$( rowOne[0] ).children('th, td').each( function (i, cell) {
 					var col = oSettings.aoColumns[i];
 			
+					if (! col) {
+						_fnLog( oSettings, 0, 'Incorrect column count', 18 );
+					}
+			
 					if ( col.mData === i ) {
 						var sort = a( cell, 'sort' ) || a( cell, 'order' );
 						var filter = a( cell, 'filter' ) || a( cell, 'search' );
@@ -3166,6 +3170,11 @@
 				create = nTrIn ? false : true;
 	
 				nTd = create ? document.createElement( oCol.sCellType ) : anTds[i];
+	
+				if (! nTd) {
+					_fnLog( oSettings, 0, 'Incorrect column count', 18 );
+				}
+	
 				nTd._DT_CellIndex = {
 					row: iRow,
 					column: i
@@ -3316,10 +3325,16 @@
 	
 			for ( i=0, ien=cells.length ; i<ien ; i++ ) {
 				column = columns[i];
-				column.nTf = cells[i].cell;
 	
-				if ( column.sClass ) {
-					$(column.nTf).addClass( column.sClass );
+				if (column) {
+					column.nTf = cells[i].cell;
+		
+					if ( column.sClass ) {
+						$(column.nTf).addClass( column.sClass );
+					}
+				}
+				else {
+					_fnLog( oSettings, 0, 'Incorrect column count', 18 );
 				}
 			}
 		}
@@ -5079,6 +5094,10 @@
 				_fnDraw( settings );
 			}
 		}
+		else {
+			// No change event - paging was called, but no change
+			_fnCallbackFire( settings, null, 'page-nc', [settings] );
+		}
 	
 		return changed;
 	}
@@ -8334,8 +8353,12 @@
 	
 	$(document).on('plugin-init.dt', function (e, context) {
 		var api = new _Api( context );
+		
+		const namespace = 'on-plugin-init';
+		const stateSaveParamsEvent = `stateSaveParams.${namespace}`;
+		const destroyEvent = `destroy.${namespace}`;
 	
-		api.on( 'stateSaveParams', function ( e, settings, d ) {
+		api.on( stateSaveParamsEvent, function ( e, settings, d ) {
 			// This could be more compact with the API, but it is a lot faster as a simple
 			// internal loop
 			var idFn = settings.rowIdFn;
@@ -8349,7 +8372,11 @@
 			}
 	
 			d.childRows = ids;
-		})
+		});
+	
+		api.on( destroyEvent, function () {
+			api.off(`${stateSaveParamsEvent} ${destroyEvent}`);
+		});
 	
 		var loaded = api.state.loaded();
 	
@@ -9670,7 +9697,7 @@
 	 *  @type string
 	 *  @default Version number
 	 */
-	DataTable.version = "1.12.1";
+	DataTable.version = "1.13.1";
 	
 	/**
 	 * Private data store, containing all of the settings objects that are
@@ -14094,7 +14121,7 @@
 		 *
 		 *  @type string
 		 */
-		build:"bs5/dt-1.12.1",
+		build:"bs5/dt-1.13.1",
 	
 	
 		/**
@@ -14732,7 +14759,7 @@
 				var classes = settings.oClasses;
 				var lang = settings.oLanguage.oPaginate;
 				var aria = settings.oLanguage.oAria.paginate || {};
-				var btnDisplay, btnClass, counter=0;
+				var btnDisplay, btnClass;
 	
 				var attach = function( container, buttons ) {
 					var i, ien, node, button, tabIndex;
@@ -14807,7 +14834,7 @@
 										'class': classes.sPageButton+' '+btnClass,
 										'aria-controls': settings.sTableId,
 										'aria-label': aria[ button ],
-										'data-dt-idx': counter,
+										'data-dt-idx': button,
 										'tabindex': tabIndex,
 										'id': idx === 0 && typeof button === 'string' ?
 											settings.sTableId +'_'+ button :
@@ -14819,8 +14846,6 @@
 								_fnBindAction(
 									node, {action: button}, clickHandler
 								);
-	
-								counter++;
 							}
 						}
 					}
@@ -15165,7 +15190,7 @@
 			}
 		}
 		else if (window.luxon) {
-			dt = format
+			dt = format && typeof d === 'string'
 				? window.luxon.DateTime.fromFormat( d, format )
 				: window.luxon.DateTime.fromISO( d );
 	
@@ -15587,7 +15612,7 @@
 	$.each( DataTable, function ( prop, val ) {
 		$.fn.DataTable[ prop ] = val;
 	} );
-	
+
 	return DataTable;
 }));
 
@@ -15596,14 +15621,6 @@
  * 2020 SpryMedia Ltd - datatables.net/license
  */
 
-/**
- * DataTables integration for Bootstrap 4. This requires Bootstrap 5 and
- * DataTables 1.10 or newer.
- *
- * This file sets the defaults and adds options to DataTables to style its
- * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
- * for further information.
- */
 (function( factory ){
 	if ( typeof define === 'function' && define.amd ) {
 		// AMD
@@ -15615,16 +15632,22 @@
 		// CommonJS
 		module.exports = function (root, $) {
 			if ( ! root ) {
+				// CommonJS environments without a window global must pass a
+				// root. This will give an error otherwise
 				root = window;
 			}
 
-			if ( ! $ || ! $.fn.dataTable ) {
-				// Require DataTables, which attaches to jQuery, including
-				// jQuery if needed and have a $ property so we can access the
-				// jQuery object that is used
-				$ = require('datatables.net')(root, $).$;
+			if ( ! $ ) {
+				$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
+					require('jquery') :
+					require('jquery')( root );
+			}
+
+			if ( ! $.fn.dataTable ) {
+				require('datatables.net')(root, $);
 			}
 
+
 			return factory( $, root, root.document );
 		};
 	}
@@ -15637,11 +15660,21 @@
 var DataTable = $.fn.dataTable;
 
 
+
+/**
+ * DataTables integration for Bootstrap 5. This requires Bootstrap 5 and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
+ * for further information.
+ */
+
 /* Set the defaults for DataTables initialisation */
 $.extend( true, DataTable.defaults, {
 	dom:
 		"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
-		"<'row'<'col-sm-12'tr>>" +
+		"<'row dt-row'<'col-sm-12'tr>>" +
 		"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
 	renderer: 'bootstrap'
 } );
@@ -15663,7 +15696,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
 	var classes = settings.oClasses;
 	var lang    = settings.oLanguage.oPaginate;
 	var aria = settings.oLanguage.oAria.paginate || {};
-	var btnDisplay, btnClass, counter=0;
+	var btnDisplay, btnClass;
 
 	var attach = function( container, buttons ) {
 		var i, ien, node, button;
@@ -15732,7 +15765,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
 								'href': '#',
 								'aria-controls': settings.sTableId,
 								'aria-label': aria[ button ],
-								'data-dt-idx': counter,
+								'data-dt-idx': button,
 								'tabindex': settings.iTabIndex,
 								'class': 'page-link'
 							} )
@@ -15743,13 +15776,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
 					settings.oApi._fnBindAction(
 						node, {action: button}, clickHandler
 					);
-
-					counter++;
 				}
 			}
 		}
 	};
 
+	var hostEl = $(host);
 	// IE9 throws an 'unknown error' if document.activeElement is used
 	// inside an iframe or frame. 
 	var activeEl;
@@ -15759,17 +15791,26 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
 		// elements, focus is lost on the select button which is bad for
 		// accessibility. So we want to restore focus once the draw has
 		// completed
-		activeEl = $(host).find(document.activeElement).data('dt-idx');
+		activeEl = hostEl.find(document.activeElement).data('dt-idx');
 	}
 	catch (e) {}
 
+	var paginationEl = hostEl.children('ul.pagination');
+
+	if (paginationEl.length) {
+		paginationEl.empty();
+	}
+	else {
+		paginationEl = hostEl.html('<ul/>').children('ul').addClass('pagination');
+	}
+
 	attach(
-		$(host).empty().html('<ul class="pagination"/>').children('ul'),
+		paginationEl,
 		buttons
 	);
 
 	if ( activeEl !== undefined ) {
-		$(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus');
+		hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus');
 	}
 };
 

+ 0 - 205
src/static/scripts/identicon.js

@@ -1,205 +0,0 @@
-/**
- * Identicon.js 2.3.3
- * http://github.com/stewartlord/identicon.js
- *
- * PNGLib required for PNG output
- * http://www.xarg.org/download/pnglib.js
- *
- * Copyright 2018, Stewart Lord
- * Released under the BSD license
- * http://www.opensource.org/licenses/bsd-license.php
- */
-
-(function() {
-    var PNGlib;
-    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
-        PNGlib = require('./pnglib');
-    } else {
-        PNGlib = window.PNGlib;
-    }
-
-    var Identicon = function(hash, options){
-        if (typeof(hash) !== 'string' || hash.length < 15) {
-            throw 'A hash of at least 15 characters is required.';
-        }
-
-        this.defaults = {
-            background: [240, 240, 240, 255],
-            margin:     0.08,
-            size:       64,
-            saturation: 0.7,
-            brightness: 0.5,
-            format:     'png'
-        };
-
-        this.options = typeof(options) === 'object' ? options : this.defaults;
-
-        // backward compatibility with old constructor (hash, size, margin)
-        if (typeof(arguments[1]) === 'number') { this.options.size   = arguments[1]; }
-        if (arguments[2])                      { this.options.margin = arguments[2]; }
-
-        this.hash        = hash
-        this.background  = this.options.background || this.defaults.background;
-        this.size        = this.options.size       || this.defaults.size;
-        this.format      = this.options.format     || this.defaults.format;
-        this.margin      = this.options.margin !== undefined ? this.options.margin : this.defaults.margin;
-
-        // foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness
-        var hue          = parseInt(this.hash.substr(-7), 16) / 0xfffffff;
-        var saturation   = this.options.saturation || this.defaults.saturation;
-        var brightness   = this.options.brightness || this.defaults.brightness;
-        this.foreground  = this.options.foreground || this.hsl2rgb(hue, saturation, brightness);
-    };
-
-    Identicon.prototype = {
-        background: null,
-        foreground: null,
-        hash:       null,
-        margin:     null,
-        size:       null,
-        format:     null,
-
-        image: function(){
-            return this.isSvg()
-                ? new Svg(this.size, this.foreground, this.background)
-                : new PNGlib(this.size, this.size, 256);
-        },
-
-        render: function(){
-            var image      = this.image(),
-                size       = this.size,
-                baseMargin = Math.floor(size * this.margin),
-                cell       = Math.floor((size - (baseMargin * 2)) / 5),
-                margin     = Math.floor((size - cell * 5) / 2),
-                bg         = image.color.apply(image, this.background),
-                fg         = image.color.apply(image, this.foreground);
-
-            // the first 15 characters of the hash control the pixels (even/odd)
-            // they are drawn down the middle first, then mirrored outwards
-            var i, color;
-            for (i = 0; i < 15; i++) {
-                color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg;
-                if (i < 5) {
-                    this.rectangle(2 * cell + margin, i * cell + margin, cell, cell, color, image);
-                } else if (i < 10) {
-                    this.rectangle(1 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
-                    this.rectangle(3 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image);
-                } else if (i < 15) {
-                    this.rectangle(0 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
-                    this.rectangle(4 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image);
-                }
-            }
-
-            return image;
-        },
-
-        rectangle: function(x, y, w, h, color, image){
-            if (this.isSvg()) {
-                image.rectangles.push({x: x, y: y, w: w, h: h, color: color});
-            } else {
-                var i, j;
-                for (i = x; i < x + w; i++) {
-                    for (j = y; j < y + h; j++) {
-                        image.buffer[image.index(i, j)] = color;
-                    }
-                }
-            }
-        },
-
-        // adapted from: https://gist.github.com/aemkei/1325937
-        hsl2rgb: function(h, s, b){
-            h *= 6;
-            s = [
-                b += s *= b < .5 ? b : 1 - b,
-                b - h % 1 * s * 2,
-                b -= s *= 2,
-                b,
-                b + h % 1 * s,
-                b + s
-            ];
-
-            return[
-                s[ ~~h    % 6 ] * 255, // red
-                s[ (h|16) % 6 ] * 255, // green
-                s[ (h|8)  % 6 ] * 255  // blue
-            ];
-        },
-
-        toString: function(raw){
-            // backward compatibility with old toString, default to base64
-            if (raw) {
-                return this.render().getDump();
-            } else {
-                return this.render().getBase64();
-            }
-        },
-
-        isSvg: function(){
-            return this.format.match(/svg/i)
-        }
-    };
-
-    var Svg = function(size, foreground, background){
-        this.size       = size;
-        this.foreground = this.color.apply(this, foreground);
-        this.background = this.color.apply(this, background);
-        this.rectangles = [];
-    };
-
-    Svg.prototype = {
-        size:       null,
-        foreground: null,
-        background: null,
-        rectangles: null,
-
-        color: function(r, g, b, a){
-            var values = [r, g, b].map(Math.round);
-            values.push((a >= 0) && (a <= 255) ? a/255 : 1);
-            return 'rgba(' + values.join(',') + ')';
-        },
-
-        getDump: function(){
-          var i,
-                xml,
-                rect,
-                fg     = this.foreground,
-                bg     = this.background,
-                stroke = this.size * 0.005;
-
-            xml = "<svg xmlns='http://www.w3.org/2000/svg'"
-                + " width='" + this.size + "' height='" + this.size + "'"
-                + " style='background-color:" + bg + ";'>"
-                + "<g style='fill:" + fg + "; stroke:" + fg + "; stroke-width:" + stroke + ";'>";
-
-            for (i = 0; i < this.rectangles.length; i++) {
-                rect = this.rectangles[i];
-                if (rect.color == bg) continue;
-                xml += "<rect "
-                    + " x='"      + rect.x + "'"
-                    + " y='"      + rect.y + "'"
-                    + " width='"  + rect.w + "'"
-                    + " height='" + rect.h + "'"
-                    + "/>";
-            }
-            xml += "</g></svg>"
-
-            return xml;
-        },
-
-        getBase64: function(){
-            if ('function' === typeof btoa) {
-                return btoa(this.getDump());
-            } else if (Buffer) {
-                return new Buffer(this.getDump(), 'binary').toString('base64');
-            } else {
-                throw 'Cannot generate base64 output';
-            }
-        }
-    };
-
-    if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
-        module.exports = Identicon;
-    } else {
-        window.Identicon = Identicon;
-    }
-})();

+ 1462 - 0
src/static/scripts/jdenticon.js

@@ -0,0 +1,1462 @@
+/**
+ * Jdenticon 3.2.0
+ * http://jdenticon.com
+ *  
+ * Built: 2022-08-07T11:23:11.640Z
+ *
+ * MIT License
+ * 
+ * Copyright (c) 2014-2021 Daniel Mester Pirttijärvi
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+(function (umdGlobal, factory) {
+    var jdenticon = factory(umdGlobal);
+
+    // Node.js
+    if (typeof module !== "undefined" && "exports" in module) {
+        module["exports"] = jdenticon;
+    }
+    // RequireJS
+    else if (typeof define === "function" && define["amd"]) {
+        define([], function () { return jdenticon; });
+    }
+    // No module loader
+    else {
+        umdGlobal["jdenticon"] = jdenticon;
+    }
+})(typeof self !== "undefined" ? self : this, function (umdGlobal) {
+'use strict';
+
+/**
+ * Parses a substring of the hash as a number.
+ * @param {number} startPosition 
+ * @param {number=} octets
+ */
+function parseHex(hash, startPosition, octets) {
+    return parseInt(hash.substr(startPosition, octets), 16);
+}
+
+function decToHex(v) {
+    v |= 0; // Ensure integer value
+    return v < 0 ? "00" :
+        v < 16 ? "0" + v.toString(16) :
+        v < 256 ? v.toString(16) :
+        "ff";
+}
+
+function hueToRgb(m1, m2, h) {
+    h = h < 0 ? h + 6 : h > 6 ? h - 6 : h;
+    return decToHex(255 * (
+        h < 1 ? m1 + (m2 - m1) * h :
+        h < 3 ? m2 :
+        h < 4 ? m1 + (m2 - m1) * (4 - h) :
+        m1));
+}
+
+/**
+ * @param {string} color  Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported.
+ * @returns {string}
+ */
+function parseColor(color) {
+    if (/^#[0-9a-f]{3,8}$/i.test(color)) {
+        var result;
+        var colorLength = color.length;
+
+        if (colorLength < 6) {
+            var r = color[1],
+                  g = color[2],
+                  b = color[3],
+                  a = color[4] || "";
+            result = "#" + r + r + g + g + b + b + a + a;
+        }
+        if (colorLength == 7 || colorLength > 8) {
+            result = color;
+        }
+        
+        return result;
+    }
+}
+
+/**
+ * Converts a hexadecimal color to a CSS3 compatible color.
+ * @param {string} hexColor  Color on the format "#RRGGBB" or "#RRGGBBAA"
+ * @returns {string}
+ */
+function toCss3Color(hexColor) {
+    var a = parseHex(hexColor, 7, 2);
+    var result;
+
+    if (isNaN(a)) {
+        result = hexColor;
+    } else {
+        var r = parseHex(hexColor, 1, 2),
+            g = parseHex(hexColor, 3, 2),
+            b = parseHex(hexColor, 5, 2);
+        result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")";
+    }
+
+    return result;
+}
+
+/**
+ * Converts an HSL color to a hexadecimal RGB color.
+ * @param {number} hue  Hue in range [0, 1]
+ * @param {number} saturation  Saturation in range [0, 1]
+ * @param {number} lightness  Lightness in range [0, 1]
+ * @returns {string}
+ */
+function hsl(hue, saturation, lightness) {
+    // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color
+    var result;
+
+    if (saturation == 0) {
+        var partialHex = decToHex(lightness * 255);
+        result = partialHex + partialHex + partialHex;
+    }
+    else {
+        var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation,
+              m1 = lightness * 2 - m2;
+        result =
+            hueToRgb(m1, m2, hue * 6 + 2) +
+            hueToRgb(m1, m2, hue * 6) +
+            hueToRgb(m1, m2, hue * 6 - 2);
+    }
+
+    return "#" + result;
+}
+
+/**
+ * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues
+ * @param {number} hue  Hue in range [0, 1]
+ * @param {number} saturation  Saturation in range [0, 1]
+ * @param {number} lightness  Lightness in range [0, 1]
+ * @returns {string}
+ */
+function correctedHsl(hue, saturation, lightness) {
+    // The corrector specifies the perceived middle lightness for each hue
+    var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ],
+          corrector = correctors[(hue * 6 + 0.5) | 0];
+    
+    // Adjust the input lightness relative to the corrector
+    lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2;
+    
+    return hsl(hue, saturation, lightness);
+}
+
+/* global umdGlobal */
+
+// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for
+// backward compatibility.
+var GLOBAL = umdGlobal;
+
+/**
+ * @typedef {Object} ParsedConfiguration
+ * @property {number} colorSaturation
+ * @property {number} grayscaleSaturation
+ * @property {string} backColor
+ * @property {number} iconPadding
+ * @property {function(number):number} hue
+ * @property {function(number):number} colorLightness
+ * @property {function(number):number} grayscaleLightness
+ */
+
+var CONFIG_PROPERTIES = {
+    G/*GLOBAL*/: "jdenticon_config",
+    n/*MODULE*/: "config",
+};
+
+var rootConfigurationHolder = {};
+
+/**
+ * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console
+ * when it is being used.
+ * @param {!Object} rootObject 
+ */
+function defineConfigProperty(rootObject) {
+    rootConfigurationHolder = rootObject;
+}
+
+/**
+ * Sets a new icon style configuration. The new configuration is not merged with the previous one. * 
+ * @param {Object} newConfiguration - New configuration object.
+ */
+function configure(newConfiguration) {
+    if (arguments.length) {
+        rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration;
+    }
+    return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/];
+}
+
+/**
+ * Gets the normalized current Jdenticon color configuration. Missing fields have default values.
+ * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A
+ *    local configuration overrides the global configuration in it entirety. This parameter can for backward
+ *    compatibility also contain a padding value. A padding value only overrides the global padding, not the
+ *    entire global configuration.
+ * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor
+ *    explicitly to the API method.
+ * @returns {ParsedConfiguration}
+ */
+function getConfiguration(paddingOrLocalConfig, defaultPadding) {
+    var configObject = 
+            typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig ||
+            rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] ||
+            GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] ||
+            { },
+
+        lightnessConfig = configObject["lightness"] || { },
+        
+        // In versions < 2.1.0 there was no grayscale saturation -
+        // saturation was the color saturation.
+        saturation = configObject["saturation"] || { },
+        colorSaturation = "color" in saturation ? saturation["color"] : saturation,
+        grayscaleSaturation = saturation["grayscale"],
+
+        backColor = configObject["backColor"],
+        padding = configObject["padding"];
+    
+    /**
+     * Creates a lightness range.
+     */
+    function lightness(configName, defaultRange) {
+        var range = lightnessConfig[configName];
+        
+        // Check if the lightness range is an array-like object. This way we ensure the
+        // array contain two values at the same time.
+        if (!(range && range.length > 1)) {
+            range = defaultRange;
+        }
+
+        /**
+         * Gets a lightness relative the specified value in the specified lightness range.
+         */
+        return function (value) {
+            value = range[0] + value * (range[1] - range[0]);
+            return value < 0 ? 0 : value > 1 ? 1 : value;
+        };
+    }
+
+    /**
+     * Gets a hue allowed by the configured hue restriction,
+     * provided the originally computed hue.
+     */
+    function hueFunction(originalHue) {
+        var hueConfig = configObject["hues"];
+        var hue;
+        
+        // Check if 'hues' is an array-like object. This way we also ensure that
+        // the array is not empty, which would mean no hue restriction.
+        if (hueConfig && hueConfig.length > 0) {
+            // originalHue is in the range [0, 1]
+            // Multiply with 0.999 to change the range to [0, 1) and then truncate the index.
+            hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)];
+        }
+
+        return typeof hue == "number" ?
+            
+            // A hue was specified. We need to convert the hue from
+            // degrees on any turn - e.g. 746° is a perfectly valid hue -
+            // to turns in the range [0, 1).
+            ((((hue / 360) % 1) + 1) % 1) :
+
+            // No hue configured => use original hue
+            originalHue;
+    }
+        
+    return {
+        X/*hue*/: hueFunction,
+        p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5,
+        H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0,
+        q/*colorLightness*/: lightness("color", [0.4, 0.8]),
+        I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]),
+        J/*backColor*/: parseColor(backColor),
+        Y/*iconPadding*/: 
+            typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : 
+            typeof padding == "number" ? padding : 
+            defaultPadding
+    }
+}
+
+var ICON_TYPE_SVG = 1;
+
+var ICON_TYPE_CANVAS = 2;
+
+var ATTRIBUTES = {
+    t/*HASH*/: "data-jdenticon-hash",
+    o/*VALUE*/: "data-jdenticon-value"
+};
+
+var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]";
+
+var documentQuerySelectorAll = /** @type {!Function} */ (
+    typeof document !== "undefined" && document.querySelectorAll.bind(document));
+
+function getIdenticonType(el) {
+    if (el) {
+        var tagName = el["tagName"];
+
+        if (/^svg$/i.test(tagName)) {
+            return ICON_TYPE_SVG;
+        }
+
+        if (/^canvas$/i.test(tagName) && "getContext" in el) {
+            return ICON_TYPE_CANVAS;
+        }
+    }
+}
+
+function observer(updateCallback) {
+    if (typeof MutationObserver != "undefined") {
+        var mutationObserver = new MutationObserver(function onmutation(mutations) {
+            for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) {
+                var mutation = mutations[mutationIndex];
+                var addedNodes = mutation.addedNodes;
+        
+                for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) {
+                    var addedNode = addedNodes[addedNodeIndex];
+        
+                    // Skip other types of nodes than element nodes, since they might not support
+                    // the querySelectorAll method => runtime error.
+                    if (addedNode.nodeType == 1) {
+                        if (getIdenticonType(addedNode)) {
+                            updateCallback(addedNode);
+                        }
+                        else {
+                            var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR);
+                            for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) {
+                                updateCallback(icons[iconIndex]);
+                            }
+                        }
+                    }
+                }
+                
+                if (mutation.type == "attributes" && getIdenticonType(mutation.target)) {
+                    updateCallback(mutation.target);
+                }
+            }
+        });
+
+        mutationObserver.observe(document.body, {
+            "childList": true,
+            "attributes": true,
+            "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"],
+            "subtree": true,
+        });
+    }
+}
+
+/**
+ * Represents a point.
+ */
+function Point(x, y) {
+    this.x = x;
+    this.y = y;
+}
+
+/**
+ * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, 
+ * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly.
+ */
+function Transform(x, y, size, rotation) {
+    this.u/*_x*/ = x;
+    this.v/*_y*/ = y;
+    this.K/*_size*/ = size;
+    this.Z/*_rotation*/ = rotation;
+}
+
+/**
+ * Transforms the specified point based on the translation and rotation specification for this Transform.
+ * @param {number} x x-coordinate
+ * @param {number} y y-coordinate
+ * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
+ * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
+ */
+Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) {
+    var right = this.u/*_x*/ + this.K/*_size*/,
+          bottom = this.v/*_y*/ + this.K/*_size*/,
+          rotation = this.Z/*_rotation*/;
+    return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) :
+           rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) :
+           rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) :
+           new Point(this.u/*_x*/ + x, this.v/*_y*/ + y);
+};
+
+var NO_TRANSFORM = new Transform(0, 0, 0, 0);
+
+
+
+/**
+ * Provides helper functions for rendering common basic shapes.
+ */
+function Graphics(renderer) {
+    /**
+     * @type {Renderer}
+     * @private
+     */
+    this.M/*_renderer*/ = renderer;
+
+    /**
+     * @type {Transform}
+     */
+    this.A/*currentTransform*/ = NO_TRANSFORM;
+}
+var Graphics__prototype = Graphics.prototype;
+
+/**
+ * Adds a polygon to the underlying renderer.
+ * @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ]
+ * @param {boolean=} invert Specifies if the polygon will be inverted.
+ */
+Graphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) {
+        var this$1 = this;
+
+    var di = invert ? -2 : 2,
+          transformedPoints = [];
+        
+    for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) {
+        transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1]));
+    }
+        
+    this.M/*_renderer*/.g/*addPolygon*/(transformedPoints);
+};
+    
+/**
+ * Adds a polygon to the underlying renderer.
+ * Source: http://stackoverflow.com/a/2173084
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse.
+ * @param {number} size The size of the ellipse.
+ * @param {boolean=} invert Specifies if the ellipse will be inverted.
+ */
+Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) {
+    var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size);
+    this.M/*_renderer*/.h/*addCircle*/(p, size, invert);
+};
+
+/**
+ * Adds a rectangle to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle.
+ * @param {number} w The width of the rectangle.
+ * @param {number} h The height of the rectangle.
+ * @param {boolean=} invert Specifies if the rectangle will be inverted.
+ */
+Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) {
+    this.g/*addPolygon*/([
+        x, y, 
+        x + w, y,
+        x + w, y + h,
+        x, y + h
+    ], invert);
+};
+
+/**
+ * Adds a right triangle to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle.
+ * @param {number} w The width of the triangle.
+ * @param {number} h The height of the triangle.
+ * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle.
+ * @param {boolean=} invert Specifies if the triangle will be inverted.
+ */
+Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) {
+    var points = [
+        x + w, y, 
+        x + w, y + h, 
+        x, y + h,
+        x, y
+    ];
+    points.splice(((r || 0) % 4) * 2, 2);
+    this.g/*addPolygon*/(points, invert);
+};
+
+/**
+ * Adds a rhombus to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus.
+ * @param {number} w The width of the rhombus.
+ * @param {number} h The height of the rhombus.
+ * @param {boolean=} invert Specifies if the rhombus will be inverted.
+ */
+Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) {
+    this.g/*addPolygon*/([
+        x + w / 2, y,
+        x + w, y + h / 2,
+        x + w / 2, y + h,
+        x, y + h / 2
+    ], invert);
+};
+
+/**
+ * @param {number} index
+ * @param {Graphics} g
+ * @param {number} cell
+ * @param {number} positionIndex
+ */
+function centerShape(index, g, cell, positionIndex) {
+    index = index % 14;
+
+    var k, m, w, h, inner, outer;
+
+    !index ? (
+        k = cell * 0.42,
+        g.g/*addPolygon*/([
+            0, 0,
+            cell, 0,
+            cell, cell - k * 2,
+            cell - k, cell,
+            0, cell
+        ])) :
+
+    index == 1 ? (
+        w = 0 | (cell * 0.5), 
+        h = 0 | (cell * 0.8),
+
+        g.j/*addTriangle*/(cell - w, 0, w, h, 2)) :
+
+    index == 2 ? (
+        w = 0 | (cell / 3),
+        g.i/*addRectangle*/(w, w, cell - w, cell - w)) :
+
+    index == 3 ? (
+        inner = cell * 0.1,
+        // Use fixed outer border widths in small icons to ensure the border is drawn
+        outer = 
+            cell < 6 ? 1 :
+            cell < 8 ? 2 :
+            (0 | (cell * 0.25)),
+        
+        inner = 
+            inner > 1 ? (0 | inner) : // large icon => truncate decimals
+            inner > 0.5 ? 1 :         // medium size icon => fixed width
+            inner,                    // small icon => anti-aliased border
+
+        g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) :
+
+    index == 4 ? (
+        m = 0 | (cell * 0.15),
+        w = 0 | (cell * 0.5),
+        g.h/*addCircle*/(cell - w - m, cell - w - m, w)) :
+
+    index == 5 ? (
+        inner = cell * 0.1,
+        outer = inner * 4,
+
+        // Align edge to nearest pixel in large icons
+        outer > 3 && (outer = 0 | outer),
+        
+        g.i/*addRectangle*/(0, 0, cell, cell),
+        g.g/*addPolygon*/([
+            outer, outer,
+            cell - inner, outer,
+            outer + (cell - outer - inner) / 2, cell - inner
+        ], true)) :
+
+    index == 6 ? 
+        g.g/*addPolygon*/([
+            0, 0,
+            cell, 0,
+            cell, cell * 0.7,
+            cell * 0.4, cell * 0.4,
+            cell * 0.7, cell,
+            0, cell
+        ]) :
+
+    index == 7 ? 
+        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
+
+    index == 8 ? (
+        g.i/*addRectangle*/(0, 0, cell, cell / 2),
+        g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2),
+        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) :
+
+    index == 9 ? (
+        inner = cell * 0.14,
+        // Use fixed outer border widths in small icons to ensure the border is drawn
+        outer = 
+            cell < 4 ? 1 :
+            cell < 6 ? 2 :
+            (0 | (cell * 0.35)),
+
+        inner = 
+            cell < 8 ? inner : // small icon => anti-aliased border
+            (0 | inner),       // large icon => truncate decimals
+
+        g.i/*addRectangle*/(0, 0, cell, cell),
+        g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) :
+
+    index == 10 ? (
+        inner = cell * 0.12,
+        outer = inner * 3,
+
+        g.i/*addRectangle*/(0, 0, cell, cell),
+        g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) :
+
+    index == 11 ? 
+        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
+
+    index == 12 ? (
+        m = cell * 0.25,
+        g.i/*addRectangle*/(0, 0, cell, cell),
+        g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) :
+
+    // 13
+    (
+        !positionIndex && (
+            m = cell * 0.4, w = cell * 1.2,
+            g.h/*addCircle*/(m, m, w)
+        )
+    );
+}
+
+/**
+ * @param {number} index
+ * @param {Graphics} g
+ * @param {number} cell
+ */
+function outerShape(index, g, cell) {
+    index = index % 4;
+
+    var m;
+
+    !index ?
+        g.j/*addTriangle*/(0, 0, cell, cell, 0) :
+        
+    index == 1 ?
+        g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) :
+
+    index == 2 ?
+        g.N/*addRhombus*/(0, 0, cell, cell) :
+
+    // 3
+    (
+        m = cell / 6,
+        g.h/*addCircle*/(m, m, cell - 2 * m)
+    );
+}
+
+/**
+ * Gets a set of identicon color candidates for a specified hue and config.
+ * @param {number} hue
+ * @param {ParsedConfiguration} config
+ */
+function colorTheme(hue, config) {
+    hue = config.X/*hue*/(hue);
+    return [
+        // Dark gray
+        correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)),
+        // Mid color
+        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)),
+        // Light gray
+        correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)),
+        // Light color
+        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)),
+        // Dark color
+        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0))
+    ];
+}
+
+/**
+ * Draws an identicon to a specified renderer.
+ * @param {Renderer} renderer
+ * @param {string} hash
+ * @param {Object|number=} config
+ */
+function iconGenerator(renderer, hash, config) {
+    var parsedConfig = getConfiguration(config, 0.08);
+
+    // Set background color
+    if (parsedConfig.J/*backColor*/) {
+        renderer.m/*setBackground*/(parsedConfig.J/*backColor*/);
+    }
+    
+    // Calculate padding and round to nearest integer
+    var size = renderer.k/*iconSize*/;
+    var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0;
+    size -= padding * 2;
+    
+    var graphics = new Graphics(renderer);
+    
+    // Calculate cell size and ensure it is an integer
+    var cell = 0 | (size / 4);
+    
+    // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon
+    var x = 0 | (padding + size / 2 - cell * 2);
+    var y = 0 | (padding + size / 2 - cell * 2);
+
+    function renderShape(colorIndex, shapes, index, rotationIndex, positions) {
+        var shapeIndex = parseHex(hash, index, 1);
+        var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0;
+        
+        renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]);
+        
+        for (var i = 0; i < positions.length; i++) {
+            graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4);
+            shapes(shapeIndex, graphics, cell, i);
+        }
+        
+        renderer.P/*endShape*/();
+    }
+
+    // AVAILABLE COLORS
+    var hue = parseHex(hash, -7) / 0xfffffff,
+    
+          // Available colors for this icon
+          availableColors = colorTheme(hue, parsedConfig),
+
+          // The index of the selected colors
+          selectedColorIndexes = [];
+
+    var index;
+
+    function isDuplicate(values) {
+        if (values.indexOf(index) >= 0) {
+            for (var i = 0; i < values.length; i++) {
+                if (selectedColorIndexes.indexOf(values[i]) >= 0) {
+                    return true;
+                }
+            }
+        }
+    }
+
+    for (var i = 0; i < 3; i++) {
+        index = parseHex(hash, 8 + i, 1) % availableColors.length;
+        if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo
+            isDuplicate([2, 3])) { // Disallow light gray and light color combo
+            index = 1;
+        }
+        selectedColorIndexes.push(index);
+    }
+
+    // ACTUAL RENDERING
+    // Sides
+    renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
+    // Corners
+    renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
+    // Center
+    renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
+    
+    renderer.finish();
+}
+
+/**
+ * Computes a SHA1 hash for any value and returns it as a hexadecimal string.
+ * 
+ * This function is optimized for minimal code size and rather short messages.
+ * 
+ * @param {string} message 
+ */
+function sha1(message) {
+    var HASH_SIZE_HALF_BYTES = 40;
+    var BLOCK_SIZE_WORDS = 16;
+
+    // Variables
+    // `var` is used to be able to minimize the number of `var` keywords.
+    var i = 0,
+        f = 0,
+    
+        // Use `encodeURI` to UTF8 encode the message without any additional libraries
+        // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky
+        // since `unescape` is deprecated.
+        urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding
+        
+        // This can be changed to a preallocated Uint32Array array for greater performance and larger code size
+        data = [],
+        dataSize,
+        
+        hashBuffer = [],
+
+        a = 0x67452301,
+        b = 0xefcdab89,
+        c = ~a,
+        d = ~b,
+        e = 0xc3d2e1f0,
+        hash = [a, b, c, d, e],
+
+        blockStartIndex = 0,
+        hexHash = "";
+
+    /**
+     * Rotates the value a specified number of bits to the left.
+     * @param {number} value  Value to rotate
+     * @param {number} shift  Bit count to shift.
+     */
+    function rotl(value, shift) {
+        return (value << shift) | (value >>> (32 - shift));
+    }
+
+    // Message data
+    for ( ; i < urlEncodedMessage.length; f++) {
+        data[f >> 2] = data[f >> 2] |
+            (
+                (
+                    urlEncodedMessage[i] == "%"
+                        // Percent encoded byte
+                        ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16)
+                        // Unencoded byte
+                        : urlEncodedMessage.charCodeAt(i++)
+                )
+
+                // Read bytes in reverse order (big endian words)
+                << ((3 - (f & 3)) * 8)
+            );
+    }
+
+    // f is now the length of the utf8 encoded message
+    // 7 = 8 bytes (64 bit) for message size, -1 to round down
+    // >> 6 = integer division with block size
+    dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS;
+
+    // Message size in bits.
+    // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least
+    // significant 32 bits are set. -8 is for the '1' bit padding byte.
+    data[dataSize - 1] = f * 8 - 8;
+    
+    // Compute hash
+    for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) {
+        for (i = 0; i < 80; i++) {
+            f = rotl(a, 5) + e + (
+                    // Ch
+                    i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 :
+                    
+                    // Parity
+                    i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 :
+                    
+                    // Maj
+                    i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc :
+                    
+                    // Parity
+                    (b ^ c ^ d) + 0xca62c1d6
+                ) + ( 
+                    hashBuffer[i] = i < BLOCK_SIZE_WORDS
+                        // Bitwise OR is used to coerse `undefined` to 0
+                        ? (data[blockStartIndex + i] | 0)
+                        : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1)
+                );
+
+            e = d;
+            d = c;
+            c = rotl(b, 30);
+            b = a;
+            a = f;
+        }
+
+        hash[0] = a = ((hash[0] + a) | 0);
+        hash[1] = b = ((hash[1] + b) | 0);
+        hash[2] = c = ((hash[2] + c) | 0);
+        hash[3] = d = ((hash[3] + d) | 0);
+        hash[4] = e = ((hash[4] + e) | 0);
+    }
+
+    // Format hex hash
+    for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) {
+        hexHash += (
+            (
+                // Get word (2^3 half-bytes per word)
+                hash[i >> 3] >>>
+
+                // Append half-bytes in reverse order
+                ((7 - (i & 7)) * 4)
+            ) 
+            // Clamp to half-byte
+            & 0xf
+        ).toString(16);
+    }
+
+    return hexHash;
+}
+
+/**
+ * Inputs a value that might be a valid hash string for Jdenticon and returns it 
+ * if it is determined valid, otherwise a falsy value is returned.
+ */
+function isValidHash(hashCandidate) {
+    return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate;
+}
+
+/**
+ * Computes a hash for the specified value. Currently SHA1 is used. This function
+ * always returns a valid hash.
+ */
+function computeHash(value) {
+    return sha1(value == null ? "" : "" + value);
+}
+
+
+
+/**
+ * Renderer redirecting drawing commands to a canvas context.
+ * @implements {Renderer}
+ */
+function CanvasRenderer(ctx, iconSize) {
+    var canvas = ctx.canvas; 
+    var width = canvas.width;
+    var height = canvas.height;
+        
+    ctx.save();
+        
+    if (!iconSize) {
+        iconSize = Math.min(width, height);
+            
+        ctx.translate(
+            ((width - iconSize) / 2) | 0,
+            ((height - iconSize) / 2) | 0);
+    }
+
+    /**
+     * @private
+     */
+    this.l/*_ctx*/ = ctx;
+    this.k/*iconSize*/ = iconSize;
+        
+    ctx.clearRect(0, 0, iconSize, iconSize);
+}
+var CanvasRenderer__prototype = CanvasRenderer.prototype;
+
+/**
+ * Fills the background with the specified color.
+ * @param {string} fillColor  Fill color on the format #rrggbb[aa].
+ */
+CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) {
+    var ctx = this.l/*_ctx*/;
+    var iconSize = this.k/*iconSize*/;
+
+    ctx.fillStyle = toCss3Color(fillColor);
+    ctx.fillRect(0, 0, iconSize, iconSize);
+};
+
+/**
+ * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.
+ * @param {string} fillColor Fill color on format #rrggbb[aa].
+ */
+CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) {
+    var ctx = this.l/*_ctx*/;
+    ctx.fillStyle = toCss3Color(fillColor);
+    ctx.beginPath();
+};
+
+/**
+ * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas.
+ */
+CanvasRenderer__prototype.P/*endShape*/ = function endShape () {
+    this.l/*_ctx*/.fill();
+};
+
+/**
+ * Adds a polygon to the rendering queue.
+ * @param points An array of Point objects.
+ */
+CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) {
+    var ctx = this.l/*_ctx*/;
+    ctx.moveTo(points[0].x, points[0].y);
+    for (var i = 1; i < points.length; i++) {
+        ctx.lineTo(points[i].x, points[i].y);
+    }
+    ctx.closePath();
+};
+
+/**
+ * Adds a circle to the rendering queue.
+ * @param {Point} point The upper left corner of the circle bounding box.
+ * @param {number} diameter The diameter of the circle.
+ * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
+ */
+CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {
+    var ctx = this.l/*_ctx*/,
+          radius = diameter / 2;
+    ctx.moveTo(point.x + radius, point.y + radius);
+    ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise);
+    ctx.closePath();
+};
+
+/**
+ * Called when the icon has been completely drawn.
+ */
+CanvasRenderer__prototype.finish = function finish () {
+    this.l/*_ctx*/.restore();
+};
+
+/**
+ * Draws an identicon to a context.
+ * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0).
+ * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
+ * @param {number} size - Icon size in pixels.
+ * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
+ *    global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
+ *    specified in place of a configuration object.
+ */
+function drawIcon(ctx, hashOrValue, size, config) {
+    if (!ctx) {
+        throw new Error("No canvas specified.");
+    }
+    
+    iconGenerator(new CanvasRenderer(ctx, size), 
+        isValidHash(hashOrValue) || computeHash(hashOrValue), 
+        config);
+}
+
+/**
+ * Prepares a measure to be used as a measure in an SVG path, by
+ * rounding the measure to a single decimal. This reduces the file
+ * size of the generated SVG with more than 50% in some cases.
+ */
+function svgValue(value) {
+    return ((value * 10 + 0.5) | 0) / 10;
+}
+
+/**
+ * Represents an SVG path element.
+ */
+function SvgPath() {
+    /**
+     * This property holds the data string (path.d) of the SVG path.
+     * @type {string}
+     */
+    this.B/*dataString*/ = "";
+}
+var SvgPath__prototype = SvgPath.prototype;
+
+/**
+ * Adds a polygon with the current fill color to the SVG path.
+ * @param points An array of Point objects.
+ */
+SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) {
+    var dataString = "";
+    for (var i = 0; i < points.length; i++) {
+        dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y);
+    }
+    this.B/*dataString*/ += dataString + "Z";
+};
+
+/**
+ * Adds a circle with the current fill color to the SVG path.
+ * @param {Point} point The upper left corner of the circle bounding box.
+ * @param {number} diameter The diameter of the circle.
+ * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
+ */
+SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {
+    var sweepFlag = counterClockwise ? 0 : 1,
+          svgRadius = svgValue(diameter / 2),
+          svgDiameter = svgValue(diameter),
+          svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " ";
+            
+    this.B/*dataString*/ += 
+        "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) +
+        svgArc + svgDiameter + ",0" + 
+        svgArc + (-svgDiameter) + ",0";
+};
+
+
+
+/**
+ * Renderer producing SVG output.
+ * @implements {Renderer}
+ */
+function SvgRenderer(target) {
+    /**
+     * @type {SvgPath}
+     * @private
+     */
+    this.C/*_path*/;
+
+    /**
+     * @type {Object.<string,SvgPath>}
+     * @private
+     */
+    this.D/*_pathsByColor*/ = { };
+
+    /**
+     * @type {SvgElement|SvgWriter}
+     * @private
+     */
+    this.R/*_target*/ = target;
+
+    /**
+     * @type {number}
+     */
+    this.k/*iconSize*/ = target.k/*iconSize*/;
+}
+var SvgRenderer__prototype = SvgRenderer.prototype;
+
+/**
+ * Fills the background with the specified color.
+ * @param {string} fillColor  Fill color on the format #rrggbb[aa].
+ */
+SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) {
+    var match = /^(#......)(..)?/.exec(fillColor),
+          opacity = match[2] ? parseHex(match[2], 0) / 255 : 1;
+    this.R/*_target*/.m/*setBackground*/(match[1], opacity);
+};
+
+/**
+ * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.
+ * @param {string} color Fill color on format #xxxxxx.
+ */
+SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) {
+    this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath());
+};
+
+/**
+ * Marks the end of the currently drawn shape.
+ */
+SvgRenderer__prototype.P/*endShape*/ = function endShape () { };
+
+/**
+ * Adds a polygon with the current fill color to the SVG.
+ * @param points An array of Point objects.
+ */
+SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) {
+    this.C/*_path*/.g/*addPolygon*/(points);
+};
+
+/**
+ * Adds a circle with the current fill color to the SVG.
+ * @param {Point} point The upper left corner of the circle bounding box.
+ * @param {number} diameter The diameter of the circle.
+ * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
+ */
+SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {
+    this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise);
+};
+
+/**
+ * Called when the icon has been completely drawn.
+ */
+SvgRenderer__prototype.finish = function finish () {
+        var this$1 = this;
+ 
+    var pathsByColor = this.D/*_pathsByColor*/;
+    for (var color in pathsByColor) {
+        // hasOwnProperty cannot be shadowed in pathsByColor
+        // eslint-disable-next-line no-prototype-builtins
+        if (pathsByColor.hasOwnProperty(color)) {
+            this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/);
+        }
+    }
+};
+
+var SVG_CONSTANTS = {
+    T/*XMLNS*/: "http://www.w3.org/2000/svg",
+    U/*WIDTH*/: "width",
+    V/*HEIGHT*/: "height",
+};
+
+/**
+ * Renderer producing SVG output.
+ */
+function SvgWriter(iconSize) {
+    /**
+     * @type {number}
+     */
+    this.k/*iconSize*/ = iconSize;
+
+    /**
+     * @type {string}
+     * @private
+     */
+    this.F/*_s*/ =
+        '<svg xmlns="' + SVG_CONSTANTS.T/*XMLNS*/ + '" width="' + 
+        iconSize + '" height="' + iconSize + '" viewBox="0 0 ' + 
+        iconSize + ' ' + iconSize + '">';
+}
+var SvgWriter__prototype = SvgWriter.prototype;
+
+/**
+ * Fills the background with the specified color.
+ * @param {string} fillColor  Fill color on the format #rrggbb.
+ * @param {number} opacity  Opacity in the range [0.0, 1.0].
+ */
+SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) {
+    if (opacity) {
+        this.F/*_s*/ += '<rect width="100%" height="100%" fill="' + 
+            fillColor + '" opacity="' + opacity.toFixed(2) + '"/>';
+    }
+};
+
+/**
+ * Writes a path to the SVG string.
+ * @param {string} color Fill color on format #rrggbb.
+ * @param {string} dataString The SVG path data string.
+ */
+SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) {
+    this.F/*_s*/ += '<path fill="' + color + '" d="' + dataString + '"/>';
+};
+
+/**
+ * Gets the rendered image as an SVG string.
+ */
+SvgWriter__prototype.toString = function toString () {
+    return this.F/*_s*/ + "</svg>";
+};
+
+/**
+ * Draws an identicon as an SVG string.
+ * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
+ * @param {number} size - Icon size in pixels.
+ * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
+ *    global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
+ *    specified in place of a configuration object.
+ * @returns {string} SVG string
+ */
+function toSvg(hashOrValue, size, config) {
+    var writer = new SvgWriter(size);
+    iconGenerator(new SvgRenderer(writer), 
+        isValidHash(hashOrValue) || computeHash(hashOrValue),
+        config);
+    return writer.toString();
+}
+
+/**
+ * Creates a new element and adds it to the specified parent.
+ * @param {Element} parentNode
+ * @param {string} name
+ * @param {...(string|number)} keyValuePairs
+ */
+function SvgElement_append(parentNode, name) {
+    var keyValuePairs = [], len = arguments.length - 2;
+    while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ];
+
+    var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name);
+    
+    for (var i = 0; i + 1 < keyValuePairs.length; i += 2) {
+        el.setAttribute(
+            /** @type {string} */(keyValuePairs[i]),
+            /** @type {string} */(keyValuePairs[i + 1])
+            );
+    }
+
+    parentNode.appendChild(el);
+}
+
+
+/**
+ * Renderer producing SVG output.
+ */
+function SvgElement(element) {
+    // Don't use the clientWidth and clientHeight properties on SVG elements
+    // since Firefox won't serve a proper value of these properties on SVG
+    // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811)
+    // Instead use 100px as a hardcoded size (the svg viewBox will rescale 
+    // the icon to the correct dimensions)
+    var iconSize = this.k/*iconSize*/ = Math.min(
+        (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100),
+        (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100)
+        );
+        
+    /**
+     * @type {Element}
+     * @private
+     */
+    this.W/*_el*/ = element;
+        
+    // Clear current SVG child elements
+    while (element.firstChild) {
+        element.removeChild(element.firstChild);
+    }
+        
+    // Set viewBox attribute to ensure the svg scales nicely.
+    element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize);
+    element.setAttribute("preserveAspectRatio", "xMidYMid meet");
+}
+var SvgElement__prototype = SvgElement.prototype;
+
+/**
+ * Fills the background with the specified color.
+ * @param {string} fillColor  Fill color on the format #rrggbb.
+ * @param {number} opacity  Opacity in the range [0.0, 1.0].
+ */
+SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) {
+    if (opacity) {
+        SvgElement_append(this.W/*_el*/, "rect",
+            SVG_CONSTANTS.U/*WIDTH*/, "100%",
+            SVG_CONSTANTS.V/*HEIGHT*/, "100%",
+            "fill", fillColor,
+            "opacity", opacity);
+    }
+};
+
+/**
+ * Appends a path to the SVG element.
+ * @param {string} color Fill color on format #xxxxxx.
+ * @param {string} dataString The SVG path data string.
+ */
+SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) {
+    SvgElement_append(this.W/*_el*/, "path",
+        "fill", color,
+        "d", dataString);
+};
+
+/**
+ * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute.
+ */
+function updateAll() {
+    if (documentQuerySelectorAll) {
+        update(ICON_SELECTOR);
+    }
+}
+
+/**
+ * Updates the identicon in the specified `<canvas>` or `<svg>` elements.
+ * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
+ *    `<svg>` or `<canvas>`, or a CSS selector to such an element.
+ * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
+ *    `data-jdenticon-value` attribute will be evaluated.
+ * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
+ *    global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be
+ *    specified in place of a configuration object.
+ */
+function update(el, hashOrValue, config) {
+    renderDomElement(el, hashOrValue, config, function (el, iconType) {
+        if (iconType) {
+            return iconType == ICON_TYPE_SVG ? 
+                new SvgRenderer(new SvgElement(el)) : 
+                new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d"));
+        }
+    });
+}
+
+/**
+ * Updates the identicon in the specified canvas or svg elements.
+ * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
+ *    `<svg>` or `<canvas>`, or a CSS selector to such an element.
+ * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
+ *    `data-jdenticon-value` attribute will be evaluated.
+ * @param {Object|number|undefined} config
+ * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer.
+ */
+function renderDomElement(el, hashOrValue, config, rendererFactory) {
+    if (typeof el === "string") {
+        if (documentQuerySelectorAll) {
+            var elements = documentQuerySelectorAll(el);
+            for (var i = 0; i < elements.length; i++) {
+                renderDomElement(elements[i], hashOrValue, config, rendererFactory);
+            }
+        }
+        return;
+    }
+    
+    // Hash selection. The result from getValidHash or computeHash is 
+    // accepted as a valid hash.
+    var hash = 
+        // 1. Explicit valid hash
+        isValidHash(hashOrValue) ||
+        
+        // 2. Explicit value (`!= null` catches both null and undefined)
+        hashOrValue != null && computeHash(hashOrValue) ||
+        
+        // 3. `data-jdenticon-hash` attribute
+        isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) ||
+        
+        // 4. `data-jdenticon-value` attribute. 
+        // We want to treat an empty attribute as an empty value. 
+        // Some browsers return empty string even if the attribute 
+        // is not specified, so use hasAttribute to determine if 
+        // the attribute is specified.
+        el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/));
+    
+    if (!hash) {
+        // No hash specified. Don't render an icon.
+        return;
+    }
+    
+    var renderer = rendererFactory(el, getIdenticonType(el));
+    if (renderer) {
+        // Draw icon
+        iconGenerator(renderer, hash, config);
+    }
+}
+
+/**
+ * Renders an identicon for all matching supported elements.
+ * 
+ * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not 
+ * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be
+ * evaluated.
+ * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global
+ * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
+ * specified in place of a configuration object.
+ */
+function jdenticonJqueryPlugin(hashOrValue, config) {
+    this["each"](function (index, el) {
+        update(el, hashOrValue, config);
+    });
+    return this;
+}
+
+// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js
+
+var jdenticon = updateAll;
+
+defineConfigProperty(jdenticon);
+
+// Export public API
+jdenticon["configure"] = configure;
+jdenticon["drawIcon"] = drawIcon;
+jdenticon["toSvg"] = toSvg;
+jdenticon["update"] = update;
+jdenticon["updateCanvas"] = update;
+jdenticon["updateSvg"] = update;
+
+/**
+ * Specifies the version of the Jdenticon package in use.
+ * @type {string}
+ */
+jdenticon["version"] = "3.2.0";
+
+/**
+ * Specifies which bundle of Jdenticon that is used.
+ * @type {string}
+ */
+jdenticon["bundle"] = "browser-umd";
+
+// Basic jQuery plugin
+var jQuery = GLOBAL["jQuery"];
+if (jQuery) {
+    jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin;
+}
+
+/**
+ * This function is called once upon page load.
+ */
+function jdenticonStartup() {
+    var replaceMode = (
+        jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] ||
+        GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] ||
+        { }
+    )["replaceMode"];
+    
+    if (replaceMode != "never") {
+        updateAll();
+        
+        if (replaceMode == "observe") {
+            observer(update);
+        }
+    }
+}
+
+// Schedule to render all identicons on the page once it has been loaded.
+if (typeof setTimeout === "function") {
+    setTimeout(jdenticonStartup, 0);
+}
+
+return jdenticon;
+
+});

+ 1 - 6
src/static/templates/admin/base.hbs

@@ -28,7 +28,7 @@
             border: var(--bs-alert-border);
         }
     </style>
-    <script src="{{urlpath}}/vw_static/identicon.js"></script>
+    <script src="{{urlpath}}/vw_static/jdenticon.js"></script>
     <script>
         'use strict';
 
@@ -45,11 +45,6 @@
             const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
             return hashHex;
         }
-        async function identicon(email) {
-            const hash = await sha256(email);
-            const data = new Identicon(hash, { size: 48, format: 'svg' });
-            return "data:image/svg+xml;base64," + data.toString();
-        }
         function toggleVis(input_id) {
             const elem = document.getElementById(input_id);
             const type = elem.getAttribute("type");

+ 1 - 7
src/static/templates/admin/organizations.hbs

@@ -16,7 +16,7 @@
                     {{#each page_data}}
                     <tr>
                         <td>
-                            <img class="float-start me-2 rounded identicon" data-src="{{Id}}">
+                            <svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{Id}}">
                             <div class="float-start">
                                 <strong>{{Name}}</strong>
                                 <span class="me-2">({{BillingEmail}})</span>
@@ -73,12 +73,6 @@
         return false;
     }
 
-    (async () => {
-        for (let e of document.querySelectorAll("img.identicon")) {
-            e.src = await identicon(e.dataset.src);
-        }
-    })();
-
     document.addEventListener("DOMContentLoaded", function() {
         $('#orgs-table').DataTable({
             "responsive": true,

+ 1 - 7
src/static/templates/admin/users.hbs

@@ -19,7 +19,7 @@
                     {{#each page_data}}
                     <tr>
                         <td>
-                            <img class="float-start me-2 rounded identicon" data-src="{{Email}}">
+                            <svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{Email}}">
                             <div class="float-start">
                                 <strong>{{Name}}</strong>
                                 <span class="d-block">{{Email}}</span>
@@ -206,12 +206,6 @@
         "3": { "name": "Manager", "color": "green" },
     };
 
-    (async () => {
-        for (let e of document.querySelectorAll("img.identicon")) {
-            e.src = await identicon(e.dataset.src);
-        }
-    })();
-
     document.querySelectorAll("[data-orgtype]").forEach(function (e) {
         let orgtype = OrgTypes[e.dataset.orgtype];
         e.style.backgroundColor = orgtype.color;