diff --git a/backend/go.mod b/backend/go.mod
index fd535ed1..06f12696 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -10,6 +10,7 @@ require (
github.com/aws/smithy-go v1.25.1
github.com/caarlos0/env/v11 v11.4.1
github.com/cenkalti/backoff/v5 v5.0.3
+ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
github.com/disintegration/imaging v1.6.2
github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1
diff --git a/backend/go.sum b/backend/go.sum
index 39cbd15f..917097ed 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -65,6 +65,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
+github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go
index 025dde67..e536517c 100644
--- a/backend/internal/bootstrap/router_bootstrap.go
+++ b/backend/internal/bootstrap/router_bootstrap.go
@@ -15,6 +15,7 @@ import (
"sync/atomic"
"time"
+ "github.com/coreos/go-systemd/activation"
"github.com/fsnotify/fsnotify"
sloggin "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
@@ -162,24 +163,26 @@ func initServer(r *gin.Engine) (*serverConfig, error) {
return nil, err
}
- network, addr := listenerNetworkAndAddr()
- listener, err := net.Listen(network, addr) //nolint:noctx
- if err != nil {
- return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
+ var socketFn func() (*socket, error)
+ switch {
+ case common.EnvConfig.SystemdSocket:
+ socketFn = systemdSocket
+ case common.EnvConfig.UnixSocket != "":
+ socketFn = unixSocket
+ default:
+ socketFn = tcpSocket
}
- if err := setUnixSocketMode(network, addr); err != nil {
- listener.Close()
+ socket, err := socketFn()
+ if err != nil {
return nil, err
}
- return &serverConfig{
- addr: addr,
- certProvider: certProvider,
- listener: listener,
- server: newHTTPServer(r, protocols),
- tlsConfig: tlsConfig,
- }, nil
+ addr := socket.addr
+ listener := socket.listener
+ server := newHTTPServer(r, protocols)
+
+ return &serverConfig{addr, certProvider, listener, server, tlsConfig}, nil
}
func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) {
@@ -207,6 +210,64 @@ func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, erro
return protocols, tlsConfig, certProvider, nil
}
+type socket struct {
+ addr string
+ listener net.Listener
+}
+
+func systemdSocket() (*socket, error) {
+ listeners, err := activation.Listeners()
+ if err != nil {
+ return nil, fmt.Errorf("failed to receive socket from systemd: %w", err)
+ }
+
+ if len(listeners) == 0 {
+ return nil, errors.New("did not receive any sockets from systemd")
+ }
+
+ if len(listeners) > 1 {
+ return nil, errors.New("received too many sockets from systemd")
+ }
+
+ return &socket{"(systemd)", listeners[0]}, nil
+}
+
+func unixSocket() (*socket, error) {
+ addr := common.EnvConfig.UnixSocket
+ os.Remove(addr) // remove dangling the socket file to avoid file-exist error
+
+ listener, err := net.Listen("unix", addr) //nolint:noctx
+ if err != nil {
+ return nil, fmt.Errorf("failed to create UNIX socket: %w", err)
+ }
+
+ if common.EnvConfig.UnixSocketMode != "" {
+ mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
+ if err != nil {
+ listener.Close()
+ return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
+ }
+
+ if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
+ listener.Close()
+ return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
+ }
+ }
+
+ return &socket{addr, listener}, nil
+}
+
+func tcpSocket() (*socket, error) {
+ addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
+
+ listener, err := net.Listen("tcp", addr) //nolint:noctx
+ if err != nil {
+ return nil, fmt.Errorf("failed to create TCP socket: %w", err)
+ }
+
+ return &socket{addr, listener}, nil
+}
+
func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
return &http.Server{
MaxHeaderBytes: 1 << 20,
@@ -226,33 +287,6 @@ func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server {
}
}
-func listenerNetworkAndAddr() (string, string) {
- if common.EnvConfig.UnixSocket == "" {
- return "tcp", net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port)
- }
-
- addr := common.EnvConfig.UnixSocket
- os.Remove(addr) // remove dangling the socket file to avoid file-exist error
- return "unix", addr
-}
-
-func setUnixSocketMode(network, addr string) error {
- if network != "unix" || common.EnvConfig.UnixSocketMode == "" {
- return nil
- }
-
- mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
- if err != nil {
- return fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
- }
-
- if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
- return fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
- }
-
- return nil
-}
-
func runServer(ctx context.Context, config *serverConfig) error {
slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil))
diff --git a/backend/internal/cmds/healthcheck.go b/backend/internal/cmds/healthcheck.go
index d32bd1f4..bbea825c 100644
--- a/backend/internal/cmds/healthcheck.go
+++ b/backend/internal/cmds/healthcheck.go
@@ -2,9 +2,12 @@ package cmds
import (
"context"
+ "fmt"
"log/slog"
+ "net"
"net/http"
"os"
+ "strings"
"time"
"github.com/spf13/cobra"
@@ -13,8 +16,14 @@ import (
)
type healthcheckFlags struct {
- Endpoint string
- Verbose bool
+ Endpoint string
+ UnixSocket string
+ Verbose bool
+}
+
+type healthcheckResult struct {
+ StatusCode int
+ URL string
}
func init() {
@@ -29,47 +38,26 @@ func init() {
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
defer cancel()
- url := flags.Endpoint + "/healthz"
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if flags.UnixSocket == "" && !cmd.Flags().Changed("endpoint") {
+ flags.UnixSocket = common.EnvConfig.UnixSocket
+ }
+
+ result, err := healthcheck(ctx, flags)
if err != nil {
slog.ErrorContext(ctx,
- "Failed to create request object",
+ "Healthcheck failed",
"error", err,
- "url", url,
"ms", time.Since(start).Milliseconds(),
)
os.Exit(1)
}
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- slog.ErrorContext(ctx,
- "Failed to perform request",
- "error", err,
- "url", url,
- "ms", time.Since(start).Milliseconds(),
- )
- os.Exit(1)
- }
- defer res.Body.Close()
-
- if res.StatusCode < 200 || res.StatusCode >= 300 {
- if err != nil {
- slog.ErrorContext(ctx,
- "Healthcheck failed",
- "status", res.StatusCode,
- "url", url,
- "ms", time.Since(start).Milliseconds(),
- )
- os.Exit(1)
- }
- }
-
if flags.Verbose {
slog.InfoContext(ctx,
"Healthcheck succeeded",
- "status", res.StatusCode,
- "url", url,
+ "status", result.StatusCode,
+ "url", result.URL,
+ "unixSocket", flags.UnixSocket,
"ms", time.Since(start).Milliseconds(),
)
}
@@ -77,7 +65,42 @@ func init() {
}
healthcheckCmd.Flags().StringVarP(&flags.Endpoint, "endpoint", "e", "http://localhost:"+common.EnvConfig.Port, "Endpoint for Pocket ID")
+ healthcheckCmd.Flags().StringVar(&flags.UnixSocket, "unix-socket", "", "UNIX socket path for Pocket ID")
healthcheckCmd.Flags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable verbose mode")
rootCmd.AddCommand(healthcheckCmd)
}
+
+func healthcheck(ctx context.Context, flags healthcheckFlags) (*healthcheckResult, error) {
+ url := strings.TrimRight(flags.Endpoint, "/") + "/healthz"
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request object for %q: %w", url, err)
+ }
+
+ client := http.DefaultClient
+ if flags.UnixSocket != "" {
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+ transport.Proxy = nil
+ transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+ dialer := net.Dialer{}
+ return dialer.DialContext(ctx, "unix", flags.UnixSocket)
+ }
+ client = &http.Client{Transport: transport}
+ }
+
+ res, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to perform request to %q: %w", url, err)
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode < 200 || res.StatusCode >= 300 {
+ return nil, fmt.Errorf("unexpected status %d from %q", res.StatusCode, url)
+ }
+
+ return &healthcheckResult{
+ StatusCode: res.StatusCode,
+ URL: url,
+ }, nil
+}
diff --git a/backend/internal/cmds/healthcheck_test.go b/backend/internal/cmds/healthcheck_test.go
new file mode 100644
index 00000000..3df83cd8
--- /dev/null
+++ b/backend/internal/cmds/healthcheck_test.go
@@ -0,0 +1,79 @@
+package cmds
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHealthcheckTCPSuccess(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/healthz", r.URL.Path)
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ defer server.Close()
+
+ result, err := healthcheck(t.Context(), healthcheckFlags{
+ Endpoint: server.URL,
+ })
+
+ require.NoError(t, err)
+ require.Equal(t, http.StatusNoContent, result.StatusCode)
+ require.Equal(t, server.URL+"/healthz", result.URL)
+}
+
+func TestHealthcheckFailsOnUnexpectedStatus(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ }))
+ defer server.Close()
+
+ _, err := healthcheck(t.Context(), healthcheckFlags{
+ Endpoint: server.URL,
+ })
+
+ require.Error(t, err)
+ require.ErrorContains(t, err, "unexpected status 500")
+}
+
+func TestHealthcheckUnixSocket(t *testing.T) {
+ socketPath := filepath.Join(t.TempDir(), "pocket-id.sock")
+ listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socketPath)
+ require.NoError(t, err)
+
+ server := &http.Server{
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/healthz", r.URL.Path)
+ w.WriteHeader(http.StatusNoContent)
+ }),
+ ReadHeaderTimeout: time.Second,
+ }
+
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- server.Serve(listener)
+ }()
+
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ require.NoError(t, server.Shutdown(ctx))
+ require.ErrorIs(t, <-errCh, http.ErrServerClosed)
+ })
+
+ result, err := healthcheck(t.Context(), healthcheckFlags{
+ Endpoint: "http://localhost:1411",
+ UnixSocket: socketPath,
+ })
+
+ require.NoError(t, err)
+ require.Equal(t, http.StatusNoContent, result.StatusCode)
+ require.Equal(t, "http://localhost:1411/healthz", result.URL)
+}
diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go
index 82a421f4..fa4f3f0c 100644
--- a/backend/internal/common/env_config.go
+++ b/backend/internal/common/env_config.go
@@ -68,6 +68,7 @@ type EnvConfigSchema struct {
Host string `env:"HOST" options:"toLower"`
UnixSocket string `env:"UNIX_SOCKET"`
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
+ SystemdSocket bool `env:"SYSTEMD_SOCKET"`
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
TLSCertFile string `env:"TLS_CERT" options:"file"`
@@ -153,6 +154,9 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
if err := validateFileBackend(config); err != nil {
return err
}
+ if config.SystemdSocket && config.UnixSocket != "" {
+ return errors.New("SYSTEMD_SOCKET and UNIX_SOCKET are mutually exclusive")
+ }
if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil {
return err
}
diff --git a/backend/internal/dto/validations.go b/backend/internal/dto/validations.go
index 7b514cd4..5578efcb 100644
--- a/backend/internal/dto/validations.go
+++ b/backend/internal/dto/validations.go
@@ -124,11 +124,11 @@ func ValidateCallbackURLPattern(raw string) bool {
}
// ValidateResponseMode validates response_mode parameter
-// If responseMode is present, it must be "form_post" or "query"
+// If responseMode is present, it must be "form_post", "query", or "fragment"
// Empty responseMode is allowed (field not provided, use default)
func ValidateResponseMode(responseMode string) bool {
switch responseMode {
- case "form_post", "query":
+ case "form_post", "query", "fragment":
return true
case "":
return true
diff --git a/backend/internal/dto/validations_test.go b/backend/internal/dto/validations_test.go
index da7e2cc4..e488468d 100644
--- a/backend/internal/dto/validations_test.go
+++ b/backend/internal/dto/validations_test.go
@@ -102,8 +102,9 @@ func TestValidateResponseMode(t *testing.T) {
}{
{"valid form_post", "form_post", true},
{"valid query", "query", true},
+ {"valid fragment", "fragment", true},
{"valid empty", "", true},
- {"invalid fragment", "fragment", false},
+ {"invalid unknown", "unknown", false},
}
for _, tt := range tests {
diff --git a/backend/internal/utils/image/profile_picture.go b/backend/internal/utils/image/profile_picture.go
index 21d78797..4c4debde 100644
--- a/backend/internal/utils/image/profile_picture.go
+++ b/backend/internal/utils/image/profile_picture.go
@@ -54,7 +54,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255})
// Load the font
- fontBytes, err := resources.FS.ReadFile("fonts/PlayfairDisplay-Bold.ttf")
+ fontBytes, err := resources.FS.ReadFile("fonts/Gloock.ttf")
if err != nil {
return nil, fmt.Errorf("failed to read font file: %w", err)
}
@@ -84,7 +84,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) {
// Center the initials
x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2
- y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() - 10
+ y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil()
drawer.Dot = fixed.P(x, y)
// Draw the initials
diff --git a/backend/resources/aaguids.json b/backend/resources/aaguids.json
index 659f9d76..5b6b630b 100644
--- a/backend/resources/aaguids.json
+++ b/backend/resources/aaguids.json
@@ -1 +1 @@
-{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key ","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","2588ae83-5a3b-4536-b8de-5e540200d191":"Dapple Authenticator from Dapple Security Inc.","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","24083bcb-3034-4867-99de-a3b52e1d426a":"Enterprise Security Key Series with NFC (Consumer Profile)","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","524de2de-982f-49b4-a769-2b5e3b73ad79":"YubiKey 5 Series (Enterprise Profile)","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","dc5e949d-f939-43b3-9877-a85c7186b753":"YubiKey Bio Multi-protocol Edition (Enterprise Profile)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","9a272558-5cfa-4424-be37-65509677b77d":"SECORA ID Key S USB by Infineon Consumer Edition","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","6d4aa745-dad5-40c4-b9b4-6a252fcee70f":"GoTrust Cyber Key","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","0a357157-9b18-4c8a-920e-d156e972b2f8":"YubiKey 5 Series (Consumer Profile)","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","7dab85a5-d16d-4eaf-a7ef-4c1385b151c5":"YubiKey 5 Series with NFC (Consumer Profile) KVZR57-2","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","eb7ef748-cbe0-4b40-b8f6-07bd2d592d35":"YubiKey 5 CCN Series with NFC (Consumer Profile)","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","22682ee5-4f0e-4c19-be3e-797d5770ebfe":"Precision InnaIT Key FIDO 2 Level 2 certified","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","6e34341a-88c7-483d-b777-a425c355be93":"WebComm OETHenticator","7a53c643-9dec-4219-b3a4-f9d24aca4e12":"G+D StarKey FIDO2-NFC","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","9955a1cd-564c-d388-ad68-9878a27be9f1":"StarSign Chase FIDO Card","9dd8d593-2213-438a-97f8-d6b813d51c27":"YubiKey Bio Fido Edition (Consumer Profile)","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","0b8b05a4-ebd4-4b0b-8f5f-33d7b6e606ab":"HID Crescendo 4000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","522a3f91-5f5d-480d-be37-6cecfad5a27b":"Precision InnaIT Key FIDO 2 Level 2 certified","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","cb4f796c-a20a-af9e-d639-213c1ec247f3":"ACS PocketKey+ Bio","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","9806a2c8-c0da-478e-b4ca-620005d34182":"YubiKey Bio Multi-protocol Edition (Consumer Profile) 1VDJSN-2","03012cb7-4fb2-42e7-9e8d-a81f10e2a5e9":"YubiKey 5 Series with Lightning (Consumer Profile)","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","0f083f18-4105-43a8-ad69-24e812e38141":"Security Key Series with NFC (Consumer Profile)","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","0ebd9f2c-f685-441c-8c3e-a02a234a840a":"YubiKey 5 Series with NFC Enhanced PIN (Consumer Profile)","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","ab7d1767-3fa0-4388-b6c4-feef7a844809":"Enterprise Security Key Series with NFC (Enterprise Profile)","f4ce5fc0-57d3-46f5-a736-efb7d5bc63b5":"YubiKey 5 Series with NFC (Consumer Profile)","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","04a8fcf2-19c1-457b-911e-69219f17583f":"Thales PAY GFCX13 authenticator","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","c1288a5c-d66b-495c-a68f-4e81f9ec5b53":"Deepnet SafeKey/Classic (FP) XF","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","ba0a9266-40d8-4048-9786-d710b5474752":"YubiKey Bio Multi-protocol Edition (Consumer Profile)","3e9db280-256a-4e17-b08e-19d79e9be166":"SECORA ID V2 by Infineon Pay Edition","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","1f8e43df-71ff-e11d-bea3-c4ee7003b232":"Thetis Pro FIDO2 Key","3ec9c8d3-a5a7-415b-a7b5-f1d606368d3f":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","9a3f2abd-a73d-439c-9ee7-1b53a857eaa7":"YubiKey 5 Series with NFC Enhanced PIN (Enterprise Profile)","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","41e39911-c669-4811-b860-c6ad0b411b96":"YubiKey 5 Series with NFC (Enterprise Profile)","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","add92433-0d69-4026-8166-29b25bce64e9":"YubiKey Bio Fido Edition (Enterprise Profile)","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","72a2b5b1-95a5-4df9-a881-4192aff4f72e":"GoTrust Idem Key mini","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","6832d205-75f2-44c7-a864-e868c796d06e":"Precision InnaIT Key FIDO 2 Level 2 certified","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","53334693-4b3f-4198-8857-53772de2ab65":"Precision InnaIT Key FIDO 2 Level 2 certified","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","357f2718-434f-4124-8a58-7e28c5e4a2fc":"Deepnet SafeKey/Classic (NFC)","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","b3315166-f36c-b05f-fea8-66a3dfdad171":"Ledger Nano Gen5 FIDO2 Authenticator","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","005b20e1-f146-4b87-8f3a-36848ff60ea6":"SECORA ID V2 by Infineon Pay Edition M","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","c3479970-e58a-4f70-836f-853bf42fb063":"YubiKey 5 Series with Lightning (Enterprise Profile)","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","87c13177-85d6-40ac-8c61-fe7ab3de9dfb":"HID Crescendo Key V3","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","d9be9d39-e6a6-4c28-a581-32b044d986e4":"Sticky Password Manager","70617373-7761-6c6c-6669-646f32303236":"Passwall","c9cadfc9-89a9-489e-a25a-c7e86a4d5f15":"Burp Suite Navigation Recorder"}
+{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key ","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","2588ae83-5a3b-4536-b8de-5e540200d191":"Dapple Authenticator from Dapple Security Inc.","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","24083bcb-3034-4867-99de-a3b52e1d426a":"Enterprise Security Key Series with NFC (Consumer Profile)","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","524de2de-982f-49b4-a769-2b5e3b73ad79":"YubiKey 5 Series (Enterprise Profile)","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","dc5e949d-f939-43b3-9877-a85c7186b753":"YubiKey Bio Multi-protocol Edition (Enterprise Profile)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","9a272558-5cfa-4424-be37-65509677b77d":"SECORA ID Key S USB by Infineon Consumer Edition","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","6d4aa745-dad5-40c4-b9b4-6a252fcee70f":"GoTrust Cyber Key","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","0a357157-9b18-4c8a-920e-d156e972b2f8":"YubiKey 5 Series (Consumer Profile)","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","7dab85a5-d16d-4eaf-a7ef-4c1385b151c5":"YubiKey 5 Series with NFC (Consumer Profile) KVZR57-2","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","eb7ef748-cbe0-4b40-b8f6-07bd2d592d35":"YubiKey 5 CCN Series with NFC (Consumer Profile)","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","22682ee5-4f0e-4c19-be3e-797d5770ebfe":"Precision InnaIT Key FIDO 2 Level 2 certified","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","6e34341a-88c7-483d-b777-a425c355be93":"WebComm OETHenticator","7a53c643-9dec-4219-b3a4-f9d24aca4e12":"G+D StarKey FIDO2-NFC","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","9955a1cd-564c-d388-ad68-9878a27be9f1":"StarSign Chase FIDO Card","9dd8d593-2213-438a-97f8-d6b813d51c27":"YubiKey Bio Fido Edition (Consumer Profile)","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","0b8b05a4-ebd4-4b0b-8f5f-33d7b6e606ab":"HID Crescendo 4000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","522a3f91-5f5d-480d-be37-6cecfad5a27b":"Precision InnaIT Key FIDO 2 Level 2 certified","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","cb4f796c-a20a-af9e-d639-213c1ec247f3":"ACS PocketKey+ Bio","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","9806a2c8-c0da-478e-b4ca-620005d34182":"YubiKey Bio Multi-protocol Edition (Consumer Profile) 1VDJSN-2","03012cb7-4fb2-42e7-9e8d-a81f10e2a5e9":"YubiKey 5 Series with Lightning (Consumer Profile)","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","0f083f18-4105-43a8-ad69-24e812e38141":"Security Key Series with NFC (Consumer Profile)","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","0ebd9f2c-f685-441c-8c3e-a02a234a840a":"YubiKey 5 Series with NFC Enhanced PIN (Consumer Profile)","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","ab7d1767-3fa0-4388-b6c4-feef7a844809":"Enterprise Security Key Series with NFC (Enterprise Profile)","f4ce5fc0-57d3-46f5-a736-efb7d5bc63b5":"YubiKey 5 Series with NFC (Consumer Profile)","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","04a8fcf2-19c1-457b-911e-69219f17583f":"Thales PAY GFCX13 authenticator","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","c1288a5c-d66b-495c-a68f-4e81f9ec5b53":"Deepnet SafeKey/Classic (FP) XF","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","ba0a9266-40d8-4048-9786-d710b5474752":"YubiKey Bio Multi-protocol Edition (Consumer Profile)","3e9db280-256a-4e17-b08e-19d79e9be166":"SECORA ID V2 by Infineon Pay Edition","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","1f8e43df-71ff-e11d-bea3-c4ee7003b232":"Thetis Pro FIDO2 Key","3ec9c8d3-a5a7-415b-a7b5-f1d606368d3f":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","9a3f2abd-a73d-439c-9ee7-1b53a857eaa7":"YubiKey 5 Series with NFC Enhanced PIN (Enterprise Profile)","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","41e39911-c669-4811-b860-c6ad0b411b96":"YubiKey 5 Series with NFC (Enterprise Profile)","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","add92433-0d69-4026-8166-29b25bce64e9":"YubiKey Bio Fido Edition (Enterprise Profile)","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","72a2b5b1-95a5-4df9-a881-4192aff4f72e":"GoTrust Idem Key mini","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","6832d205-75f2-44c7-a864-e868c796d06e":"Precision InnaIT Key FIDO 2 Level 2 certified","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","53334693-4b3f-4198-8857-53772de2ab65":"Precision InnaIT Key FIDO 2 Level 2 certified","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","357f2718-434f-4124-8a58-7e28c5e4a2fc":"Deepnet SafeKey/Classic (NFC)","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","b3315166-f36c-b05f-fea8-66a3dfdad171":"Ledger Nano Gen5 FIDO2 Authenticator","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","005b20e1-f146-4b87-8f3a-36848ff60ea6":"SECORA ID V2 by Infineon Pay Edition M","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","c3479970-e58a-4f70-836f-853bf42fb063":"YubiKey 5 Series with Lightning (Enterprise Profile)","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","87c13177-85d6-40ac-8c61-fe7ab3de9dfb":"HID Crescendo Key V3","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","9addb28c-b46f-4402-808f-019651441ff3":"KeePassPasskey","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","d9be9d39-e6a6-4c28-a581-32b044d986e4":"Sticky Password Manager","70617373-7761-6c6c-6669-646f32303236":"Passwall","c9cadfc9-89a9-489e-a25a-c7e86a4d5f15":"Burp Suite Navigation Recorder"}
diff --git a/backend/resources/fonts/Gloock.ttf b/backend/resources/fonts/Gloock.ttf
new file mode 100644
index 00000000..3e58c4e4
Binary files /dev/null and b/backend/resources/fonts/Gloock.ttf differ
diff --git a/backend/resources/fonts/PlayfairDisplay-Bold.ttf b/backend/resources/fonts/PlayfairDisplay-Bold.ttf
deleted file mode 100644
index 029a1a60..00000000
Binary files a/backend/resources/fonts/PlayfairDisplay-Bold.ttf and /dev/null differ
diff --git a/frontend/messages/tr.json b/frontend/messages/tr.json
index 537f416b..dfccd700 100644
--- a/frontend/messages/tr.json
+++ b/frontend/messages/tr.json
@@ -249,7 +249,7 @@
"edit": "Düzenle",
"user_groups_updated_successfully": "Kullanıcı grupları başarıyla güncellendi",
"user_updated_successfully": "Kullanıcı başarıyla güncellendi",
- "custom_claims_updated_successfully": "Özel alanlar başarıyla güncellendi",
+ "custom_claims_updated_successfully": "Özel yetkiler başarıyla güncellendi",
"back": "Geri",
"user_details_firstname_lastname": "Kullanıcı Detayları {firstName} {lastName}",
"manage_which_groups_this_user_belongs_to": "Bu kullanıcının ait olduğu grupları yönetin.",
diff --git a/frontend/package.json b/frontend/package.json
index 08a0178d..486cacac 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,64 +1,65 @@
{
- "name": "pocket-id-frontend",
- "version": "2.7.0",
- "private": true,
- "type": "module",
- "scripts": {
- "preinstall": "npx only-allow pnpm",
- "dev": "vite dev --port 3000",
- "build": "vite build",
- "preview": "vite preview --port 3000",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "lint": "prettier --check . && eslint .",
- "format": "prettier --write ."
- },
- "dependencies": {
- "@simplewebauthn/browser": "^13.3.0",
- "@tailwindcss/vite": "^4.3.0",
- "axios": "^1.16.1",
- "clsx": "^2.1.1",
- "date-fns": "^4.2.1",
- "jose": "^6.2.3",
- "qrcode": "^1.5.4",
- "runed": "^0.37.1",
- "sveltekit-superforms": "^2.30.1",
- "tailwind-merge": "^3.6.0",
- "zod": "^4.4.3"
- },
- "devDependencies": {
- "@inlang/paraglide-js": "^2.18.0",
- "@inlang/plugin-m-function-matcher": "^2.2.6",
- "@inlang/plugin-message-format": "^4.4.0",
- "@internationalized/date": "^3.12.1",
- "@lucide/svelte": "^1.16.0",
- "@sveltejs/adapter-static": "^3.0.10",
- "@sveltejs/kit": "^2.60.1",
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
- "@types/eslint": "^9.6.1",
- "@types/node": "^25.9.0",
- "@types/qrcode": "^1.5.6",
- "bits-ui": "^2.18.1",
- "eslint": "^10.4.0",
- "eslint-config-prettier": "^10.1.8",
- "eslint-plugin-svelte": "^3.17.1",
- "formsnap": "^2.0.1",
- "globals": "^17.6.0",
- "mode-watcher": "^1.1.0",
- "prettier": "^3.8.3",
- "prettier-plugin-svelte": "^3.5.2",
- "prettier-plugin-tailwindcss": "^0.8.0",
- "rollup": "^4.60.4",
- "svelte": "^5.55.8",
- "svelte-check": "^4.4.8",
- "svelte-sonner": "^1.1.1",
- "tailwind-variants": "^3.2.2",
- "tailwindcss": "^4.3.0",
- "tslib": "^2.8.1",
- "tw-animate-css": "^1.4.0",
- "typescript": "^6.0.3",
- "typescript-eslint": "^8.59.4",
- "vite": "^8.0.13",
- "vite-plugin-compression": "^0.5.1"
- }
+ "name": "pocket-id-frontend",
+ "version": "2.7.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "preinstall": "npx only-allow pnpm",
+ "dev": "vite dev --port 3000",
+ "build": "vite build",
+ "preview": "vite preview --port 3000",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "lint": "prettier --check . && eslint .",
+ "format": "prettier --write ."
+ },
+ "dependencies": {
+ "@fontsource/gloock": "^5.2.8",
+ "@simplewebauthn/browser": "^13.3.0",
+ "@tailwindcss/vite": "^4.3.0",
+ "axios": "^1.16.1",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.2.1",
+ "jose": "^6.2.3",
+ "qrcode": "^1.5.4",
+ "runed": "^0.37.1",
+ "sveltekit-superforms": "^2.30.1",
+ "tailwind-merge": "^3.6.0",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@inlang/paraglide-js": "^2.18.0",
+ "@inlang/plugin-m-function-matcher": "^2.2.6",
+ "@inlang/plugin-message-format": "^4.4.0",
+ "@internationalized/date": "^3.12.1",
+ "@lucide/svelte": "^1.16.0",
+ "@sveltejs/adapter-static": "^3.0.10",
+ "@sveltejs/kit": "^2.60.1",
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
+ "@types/eslint": "^9.6.1",
+ "@types/node": "^25.9.0",
+ "@types/qrcode": "^1.5.6",
+ "bits-ui": "^2.18.1",
+ "eslint": "^10.4.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-svelte": "^3.17.1",
+ "formsnap": "^2.0.1",
+ "globals": "^17.6.0",
+ "mode-watcher": "^1.1.0",
+ "prettier": "^3.8.3",
+ "prettier-plugin-svelte": "^3.5.2",
+ "prettier-plugin-tailwindcss": "^0.8.0",
+ "rollup": "^4.60.4",
+ "svelte": "^5.55.8",
+ "svelte-check": "^4.4.8",
+ "svelte-sonner": "^1.1.1",
+ "tailwind-variants": "^3.2.2",
+ "tailwindcss": "^4.3.0",
+ "tslib": "^2.8.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.59.4",
+ "vite": "^8.0.13",
+ "vite-plugin-compression": "^0.5.1"
+ }
}
diff --git a/frontend/src/app.css b/frontend/src/app.css
index a29105f9..27e1d304 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -1,5 +1,6 @@
@import 'tailwindcss';
@import 'tw-animate-css';
+@import '@fontsource/gloock';
@variant dark (&:where(.dark, .dark *));
@@ -22,7 +23,7 @@
}
:root {
- --radius: 0.625rem;
+ --radius: 1rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -133,13 +134,8 @@
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
- /* Animations */
- --animate-accordion-up: accordion-up 0.2s ease-out;
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-caret-blink: caret-blink 1.25s ease-out infinite;
-
/* Font */
- --font-playfair: 'Playfair Display', serif;
+ --font-glooock: 'glooock ', serif;
}
@layer base {
@@ -154,60 +150,6 @@
button {
@apply cursor-pointer;
}
-
- @font-face {
- font-family: 'Playfair Display';
- font-weight: 400;
- src: url('/fonts/PlayfairDisplay-Regular.woff') format('woff');
- }
- @font-face {
- font-family: 'Playfair Display';
- font-weight: 500;
- src: url('/fonts/PlayfairDisplay-Medium.woff') format('woff');
- }
- @font-face {
- font-family: 'Playfair Display';
- font-weight: 600;
- src: url('/fonts/PlayfairDisplay-SemiBold.woff') format('woff');
- }
- @font-face {
- font-family: 'Playfair Display';
- font-weight: 700;
- src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
- }
-}
-
-@keyframes accordion-down {
- from {
- height: 0;
- }
-
- to {
- height: var(--bits-accordion-content-height);
- }
-}
-
-@keyframes accordion-up {
- from {
- height: var(--bits-accordion-content-height);
- }
-
- to {
- height: 0;
- }
-}
-
-@keyframes caret-blink {
- 0%,
- 70%,
- 100% {
- opacity: 1;
- }
-
- 20%,
- 50% {
- opacity: 0;
- }
}
.animate-fade-in {
@@ -220,6 +162,7 @@
opacity: 0;
transform: translateY(10px);
}
+
to {
opacity: 1;
transform: translateY(0);
@@ -230,6 +173,7 @@
0% {
transform: scale(1.3);
}
+
100% {
transform: scale(1);
}
@@ -246,6 +190,7 @@
40% {
opacity: 0;
}
+
100% {
opacity: 1;
}
diff --git a/frontend/src/lib/components/form/searchable-multi-select.svelte b/frontend/src/lib/components/form/searchable-multi-select.svelte
index d2fb24f2..343ab32e 100644
--- a/frontend/src/lib/components/form/searchable-multi-select.svelte
+++ b/frontend/src/lib/components/form/searchable-multi-select.svelte
@@ -81,18 +81,20 @@
variant="outline"
role="combobox"
aria-expanded={open}
- class="h-auto min-h-10 w-full justify-between"
+ class="h-auto min-h-10 w-full bg-input/30!"
>
-
- {#if selectedItems.length > 0}
- {#each selectedLabels as label}
-
{label}
- {/each}
- {:else}
-
{m.select_items()}
- {/if}
+
+
+ {#if selectedItems.length > 0}
+ {#each selectedLabels as label}
+ {label}
+ {/each}
+ {:else}
+ {m.select_items()}
+ {/if}
+
+
-
{/snippet}
diff --git a/frontend/src/lib/components/form/searchable-select.svelte b/frontend/src/lib/components/form/searchable-select.svelte
index cfc2301f..1b9dcaa8 100644
--- a/frontend/src/lib/components/form/searchable-select.svelte
+++ b/frontend/src/lib/components/form/searchable-select.svelte
@@ -70,10 +70,12 @@
role="combobox"
aria-expanded={open}
{...props}
- class={cn('justify-between', restProps.class)}
+ class={restProps.class}
>
- {items.find((item) => item.value === value)?.label || selectText}
-
+
+ {items.find((item) => item.value === value)?.label || selectText}
+
+
{/snippet}
diff --git a/frontend/src/lib/components/header/header.svelte b/frontend/src/lib/components/header/header.svelte
index e3b0625a..94336270 100644
--- a/frontend/src/lib/components/header/header.svelte
+++ b/frontend/src/lib/components/header/header.svelte
@@ -1,8 +1,9 @@
-
+
{#if !isAuthPage}
-
+
-
- {$appConfigStore.appName}
+
+
+ {m.settings()}
{/if}
diff --git a/frontend/src/lib/components/passkey-row.svelte b/frontend/src/lib/components/passkey-row.svelte
index 77dde69c..b5777040 100644
--- a/frontend/src/lib/components/passkey-row.svelte
+++ b/frontend/src/lib/components/passkey-row.svelte
@@ -23,7 +23,7 @@
-
+
{#if icon}{@const Icon = icon}
{/if}
diff --git a/frontend/src/lib/components/qrcode/qrcode.svelte b/frontend/src/lib/components/qrcode/qrcode.svelte
index 9b4984a7..868525ad 100644
--- a/frontend/src/lib/components/qrcode/qrcode.svelte
+++ b/frontend/src/lib/components/qrcode/qrcode.svelte
@@ -9,7 +9,7 @@
value,
size = 200,
color = '#000000',
- backgroundColor = '#FFFFFF',
+ backgroundColor = 'transparent',
...restProps
}: HTMLAttributes & {
value: string | null;
@@ -39,4 +39,4 @@
});
-
+
diff --git a/frontend/src/lib/components/signup/signup-token-modal.svelte b/frontend/src/lib/components/signup/signup-token-modal.svelte
index 3456f09c..ab1f79fe 100644
--- a/frontend/src/lib/components/signup/signup-token-modal.svelte
+++ b/frontend/src/lib/components/signup/signup-token-modal.svelte
@@ -187,7 +187,6 @@
value={signupLink}
size={180}
color={mode.current === 'dark' ? '#FFFFFF' : '#000000'}
- backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'}
/>
diff --git a/frontend/src/lib/components/table/advanced-table-column-selection.svelte b/frontend/src/lib/components/table/advanced-table-column-selection.svelte
index 0fc5d329..c0839476 100644
--- a/frontend/src/lib/components/table/advanced-table-column-selection.svelte
+++ b/frontend/src/lib/components/table/advanced-table-column-selection.svelte
@@ -16,7 +16,7 @@
class={buttonVariants({
variant: 'outline',
size: 'sm',
- class: 'ml-auto h-8'
+ class: 'ml-auto'
})}
>
diff --git a/frontend/src/lib/components/table/advanced-table-filter.svelte b/frontend/src/lib/components/table/advanced-table-filter.svelte
index 4e1b5609..57d6c23c 100644
--- a/frontend/src/lib/components/table/advanced-table-filter.svelte
+++ b/frontend/src/lib/components/table/advanced-table-filter.svelte
@@ -38,11 +38,13 @@
{...props}
variant="outline"
size="sm"
- class="h-8 border-dashed"
+ class="order-dashed"
data-testid={`facet-${title.toLowerCase()}-trigger`}
>
-
- {title}
+
+
+ {title}
+
{#if selectedValues.size > 0}
diff --git a/frontend/src/lib/components/table/advanced-table.svelte b/frontend/src/lib/components/table/advanced-table.svelte
index d1625250..c30b03a0 100644
--- a/frontend/src/lib/components/table/advanced-table.svelte
+++ b/frontend/src/lib/components/table/advanced-table.svelte
@@ -243,7 +243,7 @@
class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent"
onclick={() => onSort(column.column)}
>
-
+
{column.label}
action.onClick(item)}
disabled={action.disabled}
- class={action.variant === 'danger'
- ? 'text-red-500 focus:!text-red-700'
- : ''}
+ class={action.variant === 'danger' ? 'text-red-500!' : ''}
>
{#if action.icon}
{@const Icon = action.icon}
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte
index 1f849e51..946a1870 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte
@@ -1,18 +1,27 @@
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte
index dac534d5..f3089db6 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte
@@ -1,18 +1,27 @@
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte
index a9808dae..9d3e9585 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte
@@ -1,27 +1,32 @@
-
+
-
+
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte
index 4622dec1..3d6ede31 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte
@@ -12,6 +12,9 @@
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte
index 8a7c1611..09d285f5 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte
@@ -13,7 +13,10 @@
{@render children?.()}
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte
index 484c948b..419a434b 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte
@@ -13,7 +13,10 @@
{@render children?.()}
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte
new file mode 100644
index 00000000..ec2baca5
--- /dev/null
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte
index 4e1d6fe8..5e974556 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte
@@ -13,7 +13,7 @@
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
- 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
+ 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50',
className
)}
{...restProps}
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte
new file mode 100644
index 00000000..dd3ac7d9
--- /dev/null
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
index 0bc78202..f8c458ea 100644
--- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
@@ -12,6 +12,9 @@
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte
new file mode 100644
index 00000000..cde7911a
--- /dev/null
+++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts
index bc8e9709..260a5441 100644
--- a/frontend/src/lib/components/ui/alert-dialog/index.ts
+++ b/frontend/src/lib/components/ui/alert-dialog/index.ts
@@ -1,4 +1,5 @@
-import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
+import Root from './alert-dialog.svelte';
+import Portal from './alert-dialog-portal.svelte';
import Trigger from './alert-dialog-trigger.svelte';
import Title from './alert-dialog-title.svelte';
import Action from './alert-dialog-action.svelte';
@@ -8,29 +9,32 @@ import Header from './alert-dialog-header.svelte';
import Overlay from './alert-dialog-overlay.svelte';
import Content from './alert-dialog-content.svelte';
import Description from './alert-dialog-description.svelte';
-
-const Root = AlertDialogPrimitive.Root;
+import Media from './alert-dialog-media.svelte';
export {
Root,
Title,
Action,
Cancel,
+ Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
+ Media,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
+ Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
- Description as AlertDialogDescription
+ Description as AlertDialogDescription,
+ Media as AlertDialogMedia
};
diff --git a/frontend/src/lib/components/ui/alert/alert.svelte b/frontend/src/lib/components/ui/alert/alert.svelte
index 09e6d8ac..c09ed4ac 100644
--- a/frontend/src/lib/components/ui/alert/alert.svelte
+++ b/frontend/src/lib/components/ui/alert/alert.svelte
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const alertVariants = tv({
- base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+ base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-3xl border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
variants: {
variant: {
default: 'bg-card text-card-foreground',
diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte
index d9e34464..1a3c5b5c 100644
--- a/frontend/src/lib/components/ui/badge/badge.svelte
+++ b/frontend/src/lib/components/ui/badge/badge.svelte
@@ -2,15 +2,16 @@
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
- base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
+ base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
variants: {
variant: {
- default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
- secondary:
- 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
+ secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
destructive:
- 'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
- outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
+ 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
+ outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
+ ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
+ link: 'text-primary underline-offset-4 hover:underline'
}
},
defaultVariants: {
diff --git a/frontend/src/lib/components/ui/button/button.svelte b/frontend/src/lib/components/ui/button/button.svelte
index ae785f3d..2e640acd 100644
--- a/frontend/src/lib/components/ui/button/button.svelte
+++ b/frontend/src/lib/components/ui/button/button.svelte
@@ -4,22 +4,24 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const buttonVariants = tv({
- base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:shrink-0 transition active:scale-95",
variants: {
variant: {
- default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
- destructive:
- 'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
outline:
- 'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
- secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ 'border border-border bg-background hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
+ ghost:
+ 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
+ destructive:
+ 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
- sm: 'h-8 rounded-md px-3',
- lg: 'h-11 rounded-md px-8',
+ sm: 'h-9 px-3 py-2',
+ lg: 'h-11 px-8',
icon: 'h-10 w-10'
}
},
@@ -96,9 +98,6 @@
tabindex={disabled ? -1 : undefined}
{...restProps}
>
- {#if isLoading}
-
- {/if}
{@render children?.()}
{:else}
@@ -111,9 +110,15 @@
onclick={handleOnClick}
{...restProps}
>
- {#if isLoading}
-
- {/if}
- {@render children?.()}
+
+
+ {@render children?.()}
+
{/if}
diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte
index 23f876d6..536744ca 100644
--- a/frontend/src/lib/components/ui/card/card.svelte
+++ b/frontend/src/lib/components/ui/card/card.svelte
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card"
class={cn(
- 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
+ 'bg-card text-card-foreground flex flex-col gap-6 rounded-4xl border py-6 shadow-sm',
className
)}
{...restProps}
diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte
index b4aba473..4dce737b 100644
--- a/frontend/src/lib/components/ui/dialog/dialog-content.svelte
+++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte
@@ -23,14 +23,14 @@
bind:ref
data-slot="dialog-content"
class={cn(
- 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
+ 'bg-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-4xl border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...restProps}
>
{@render children?.()}
Close
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte
new file mode 100644
index 00000000..c0bf94b5
--- /dev/null
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
index 74e21bb8..8ff36900 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
@@ -1,7 +1,7 @@
-
+
-
+
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
index 231aee49..5115173e 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
@@ -17,6 +17,6 @@
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
- class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
+ class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)}
{...restProps}
/>
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
index 204a67d0..405fbe2e 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -20,7 +20,7 @@
data-inset={inset}
data-variant={variant}
class={cn(
- "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-2 py-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
index fe2ef4a7..ed35bc77 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
@@ -17,7 +17,10 @@
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
- class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
+ class={cn(
+ 'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8',
+ className
+ )}
{...restProps}
>
{@render children?.()}
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte
new file mode 100644
index 00000000..089a25cc
--- /dev/null
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
index d4171b97..4c06cd7f 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
@@ -1,6 +1,6 @@
+
+
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
new file mode 100644
index 00000000..fbbb7908
--- /dev/null
+++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts
index aeb398e0..b097e3fd 100644
--- a/frontend/src/lib/components/ui/dropdown-menu/index.ts
+++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts
@@ -1,4 +1,6 @@
-import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
+import Root from './dropdown-menu.svelte';
+import Sub from './dropdown-menu-sub.svelte';
+import CheckboxGroup from './dropdown-menu-checkbox-group.svelte';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import Group from './dropdown-menu-group.svelte';
@@ -12,15 +14,18 @@ import Trigger from './dropdown-menu-trigger.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte';
-const Sub = DropdownMenuPrimitive.Sub;
-const Root = DropdownMenuPrimitive.Root;
+import Portal from './dropdown-menu-portal.svelte';
export {
+ CheckboxGroup,
CheckboxItem,
Content,
+ Portal,
Root as DropdownMenu,
+ CheckboxGroup as DropdownMenuCheckboxGroup,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
+ Portal as DropdownMenuPortal,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte
index 8f865f96..e92db9e7 100644
--- a/frontend/src/lib/components/ui/input/input.svelte
+++ b/frontend/src/lib/components/ui/input/input.svelte
@@ -24,7 +24,7 @@
bind:this={ref}
data-slot="input"
class={cn(
- 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+ 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
@@ -39,7 +39,7 @@
bind:this={ref}
data-slot="input"
class={cn(
- 'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+ 'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
diff --git a/frontend/src/lib/components/ui/item/item.svelte b/frontend/src/lib/components/ui/item/item.svelte
index 4839643e..8f0f4e76 100644
--- a/frontend/src/lib/components/ui/item/item.svelte
+++ b/frontend/src/lib/components/ui/item/item.svelte
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const itemVariants = tv({
- base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]',
+ base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-4xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]',
variants: {
variant: {
default: 'bg-transparent [a&]:hover:bg-accent/50 [a&]:transition-colors',
@@ -12,8 +12,8 @@
transparent: 'bg-transparent'
},
size: {
- default: 'gap-4 p-4',
- sm: 'gap-2.5 px-4 py-3'
+ default: 'gap-4 p-6',
+ sm: 'gap-2.5 px-4 py-4'
}
},
defaultVariants: {
diff --git a/frontend/src/lib/components/ui/popover/index.ts b/frontend/src/lib/components/ui/popover/index.ts
index 0dcc3ccb..7ca18f4c 100644
--- a/frontend/src/lib/components/ui/popover/index.ts
+++ b/frontend/src/lib/components/ui/popover/index.ts
@@ -1,17 +1,28 @@
-import { Popover as PopoverPrimitive } from 'bits-ui';
+import Root from './popover.svelte';
+import Close from './popover-close.svelte';
import Content from './popover-content.svelte';
+import Description from './popover-description.svelte';
+import Header from './popover-header.svelte';
+import Title from './popover-title.svelte';
import Trigger from './popover-trigger.svelte';
-const Root = PopoverPrimitive.Root;
-const Close = PopoverPrimitive.Close;
+import Portal from './popover-portal.svelte';
export {
Root,
Content,
+ Description,
+ Header,
+ Title,
Trigger,
Close,
+ Portal,
//
Root as Popover,
Content as PopoverContent,
+ Description as PopoverDescription,
+ Header as PopoverHeader,
+ Title as PopoverTitle,
Trigger as PopoverTrigger,
- Close as PopoverClose
+ Close as PopoverClose,
+ Portal as PopoverPortal
};
diff --git a/frontend/src/lib/components/ui/popover/popover-close.svelte b/frontend/src/lib/components/ui/popover/popover-close.svelte
new file mode 100644
index 00000000..dc4dec4b
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover-close.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/popover/popover-content.svelte b/frontend/src/lib/components/ui/popover/popover-content.svelte
index 95e209a5..58ef2c5d 100644
--- a/frontend/src/lib/components/ui/popover/popover-content.svelte
+++ b/frontend/src/lib/components/ui/popover/popover-content.svelte
@@ -1,6 +1,8 @@
-
+
-
+
diff --git a/frontend/src/lib/components/ui/popover/popover-description.svelte b/frontend/src/lib/components/ui/popover/popover-description.svelte
new file mode 100644
index 00000000..a560005f
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover-description.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/popover/popover-header.svelte b/frontend/src/lib/components/ui/popover/popover-header.svelte
new file mode 100644
index 00000000..b64b0906
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover-header.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/popover/popover-portal.svelte b/frontend/src/lib/components/ui/popover/popover-portal.svelte
new file mode 100644
index 00000000..25efb877
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/popover/popover-title.svelte b/frontend/src/lib/components/ui/popover/popover-title.svelte
new file mode 100644
index 00000000..1ca16a96
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover-title.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/popover/popover.svelte b/frontend/src/lib/components/ui/popover/popover.svelte
new file mode 100644
index 00000000..f39b867a
--- /dev/null
+++ b/frontend/src/lib/components/ui/popover/popover.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts
index e6acdf8e..305c47ad 100644
--- a/frontend/src/lib/components/ui/select/index.ts
+++ b/frontend/src/lib/components/ui/select/index.ts
@@ -1,5 +1,4 @@
-import { Select as SelectPrimitive } from 'bits-ui';
-
+import Root from './select.svelte';
import Group from './select-group.svelte';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
@@ -8,8 +7,8 @@ import Trigger from './select-trigger.svelte';
import Separator from './select-separator.svelte';
import ScrollDownButton from './select-scroll-down-button.svelte';
import ScrollUpButton from './select-scroll-up-button.svelte';
-
-const Root = SelectPrimitive.Root;
+import GroupHeading from './select-group-heading.svelte';
+import Portal from './select-portal.svelte';
export {
Root,
@@ -21,6 +20,8 @@ export {
Separator,
ScrollDownButton,
ScrollUpButton,
+ GroupHeading,
+ Portal,
//
Root as Select,
Group as SelectGroup,
@@ -30,5 +31,7 @@ export {
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
- ScrollUpButton as SelectScrollUpButton
+ ScrollUpButton as SelectScrollUpButton,
+ GroupHeading as SelectGroupHeading,
+ Portal as SelectPortal
};
diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte
index 9450e3be..edb50d06 100644
--- a/frontend/src/lib/components/ui/select/select-content.svelte
+++ b/frontend/src/lib/components/ui/select/select-content.svelte
@@ -1,8 +1,11 @@
-
+
{@render children?.()}
-
+
diff --git a/frontend/src/lib/components/ui/select/select-group-heading.svelte b/frontend/src/lib/components/ui/select/select-group-heading.svelte
new file mode 100644
index 00000000..8a5aa3b1
--- /dev/null
+++ b/frontend/src/lib/components/ui/select/select-group-heading.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/select/select-group.svelte b/frontend/src/lib/components/ui/select/select-group.svelte
index 2520795d..bb26231a 100644
--- a/frontend/src/lib/components/ui/select/select-group.svelte
+++ b/frontend/src/lib/components/ui/select/select-group.svelte
@@ -1,7 +1,17 @@
-
+
diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte
index 4bf38167..f40cb30b 100644
--- a/frontend/src/lib/components/ui/select/select-item.svelte
+++ b/frontend/src/lib/components/ui/select/select-item.svelte
@@ -1,7 +1,7 @@
+
+
diff --git a/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte
index 7ede728a..a9d95bb9 100644
--- a/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte
+++ b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte
@@ -1,7 +1,7 @@
+
+
diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte
index 9e2570b2..f2a2ef2d 100644
--- a/frontend/src/lib/components/ui/separator/separator.svelte
+++ b/frontend/src/lib/components/ui/separator/separator.svelte
@@ -5,15 +5,18 @@
let {
ref = $bindable(null),
class: className,
+ 'data-slot': dataSlot = 'separator',
...restProps
}: SeparatorPrimitive.RootProps = $props();
diff --git a/frontend/src/lib/components/ui/tabs/index.ts b/frontend/src/lib/components/ui/tabs/index.ts
index d2a7939e..2ea36233 100644
--- a/frontend/src/lib/components/ui/tabs/index.ts
+++ b/frontend/src/lib/components/ui/tabs/index.ts
@@ -1,6 +1,6 @@
import Root from './tabs.svelte';
import Content from './tabs-content.svelte';
-import List from './tabs-list.svelte';
+import List, { tabsListVariants, type TabsListVariant } from './tabs-list.svelte';
import Trigger from './tabs-trigger.svelte';
export {
@@ -8,6 +8,8 @@ export {
Content,
List,
Trigger,
+ tabsListVariants,
+ type TabsListVariant,
//
Root as Tabs,
Content as TabsContent,
diff --git a/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/frontend/src/lib/components/ui/tabs/tabs-content.svelte
index 35e44f66..07ab3308 100644
--- a/frontend/src/lib/components/ui/tabs/tabs-content.svelte
+++ b/frontend/src/lib/components/ui/tabs/tabs-content.svelte
@@ -12,6 +12,6 @@
diff --git a/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/frontend/src/lib/components/ui/tabs/tabs-list.svelte
index fffdc1f8..24177222 100644
--- a/frontend/src/lib/components/ui/tabs/tabs-list.svelte
+++ b/frontend/src/lib/components/ui/tabs/tabs-list.svelte
@@ -1,16 +1,40 @@
-
+
+
diff --git a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte
index 3766b0ad..c0a8cd18 100644
--- a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte
+++ b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte
@@ -1,6 +1,6 @@
-
+
-
+
diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte
new file mode 100644
index 00000000..7d328f01
--- /dev/null
+++ b/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte
new file mode 100644
index 00000000..fde5bb28
--- /dev/null
+++ b/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/tooltip/tooltip.svelte b/frontend/src/lib/components/ui/tooltip/tooltip.svelte
new file mode 100644
index 00000000..7c9d7125
--- /dev/null
+++ b/frontend/src/lib/components/ui/tooltip/tooltip.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/frontend/src/lib/components/web-authn-unsupported.svelte b/frontend/src/lib/components/web-authn-unsupported.svelte
index 5bdc97d1..bec0709d 100644
--- a/frontend/src/lib/components/web-authn-unsupported.svelte
+++ b/frontend/src/lib/components/web-authn-unsupported.svelte
@@ -7,7 +7,7 @@
- {m.browser_unsupported()}
+ {m.browser_unsupported()}
{m.this_browser_does_not_support_passkeys()}
diff --git a/frontend/src/routes/authorize/+page.svelte b/frontend/src/routes/authorize/+page.svelte
index 4909541e..af79cd69 100644
--- a/frontend/src/routes/authorize/+page.svelte
+++ b/frontend/src/routes/authorize/+page.svelte
@@ -180,11 +180,10 @@
throw new Error('Invalid redirect URL protocol');
}
- redirectURL.searchParams.append('error', error);
- if (authorizeState) {
- redirectURL.searchParams.append('state', authorizeState);
- }
- window.location.href = redirectURL.toString();
+ window.location.href = createRedirectURL(callbackURL, {
+ error,
+ state: authorizeState
+ });
}
function onSuccess(code: string, callbackURL: string, issuer: string) {
@@ -193,10 +192,6 @@
throw new Error('Invalid redirect URL protocol');
}
- redirectURL.searchParams.append('code', code);
- redirectURL.searchParams.append('state', authorizeState);
- redirectURL.searchParams.append('iss', issuer);
-
success = true;
setTimeout(() => {
if (responseMode === 'form_post') {
@@ -231,16 +226,34 @@
document.body.appendChild(form);
form.submit();
} else {
- // Default query parameter redirect (response_mode=query or not specified)
- const redirectURL = new URL(callbackURL);
- redirectURL.searchParams.append('code', code);
- redirectURL.searchParams.append('state', authorizeState);
- redirectURL.searchParams.append('iss', issuer);
-
- window.location.href = redirectURL.toString();
+ window.location.href = createRedirectURL(callbackURL, {
+ code,
+ state: authorizeState,
+ iss: issuer
+ });
}
}, 1000);
}
+
+ function createRedirectURL(url: string, params: Record) {
+ const redirectURL = new URL(url);
+ const responseParams =
+ responseMode === 'fragment'
+ ? new URLSearchParams(redirectURL.hash.slice(1))
+ : redirectURL.searchParams;
+
+ for (const [key, value] of Object.entries(params)) {
+ if (value) {
+ responseParams.set(key, value);
+ }
+ }
+
+ if (responseMode === 'fragment') {
+ redirectURL.hash = responseParams.toString();
+ }
+
+ return redirectURL.toString();
+ }
@@ -252,7 +265,7 @@
{:else}
-
+
{m.sign_in_to({ name: client.name })}
{#if errorMessage}
@@ -260,7 +273,22 @@
{errorMessage}.
{/if}
- {#if accountSelectionRequired && $userStore && !errorMessage}
+ {#if authorizationRequired}
+
+ {:else if accountSelectionRequired && $userStore && !errorMessage}
@@ -301,21 +329,6 @@
})}
/>
- {:else if authorizationRequired}
-
{/if}
diff --git a/frontend/src/routes/device/+page.svelte b/frontend/src/routes/device/+page.svelte
index 72b955de..bdc6d377 100644
--- a/frontend/src/routes/device/+page.svelte
+++ b/frontend/src/routes/device/+page.svelte
@@ -79,7 +79,7 @@
{/if}
-
{m.authorize_device()}
+
{m.authorize_device()}
{#if errorMessage}
{errorMessage}. {m.please_try_again()}
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte
index 597cc791..0daedab0 100644
--- a/frontend/src/routes/login/+page.svelte
+++ b/frontend/src/routes/login/+page.svelte
@@ -44,7 +44,7 @@
-
+
{m.sign_in_to_appname({ appName: $appConfigStore.appName })}
{#if error}
diff --git a/frontend/src/routes/login/alternative/+page.svelte b/frontend/src/routes/login/alternative/+page.svelte
index 033db476..704dc3a3 100644
--- a/frontend/src/routes/login/alternative/+page.svelte
+++ b/frontend/src/routes/login/alternative/+page.svelte
@@ -35,7 +35,7 @@
- {m.alternative_sign_in()}
+ {m.alternative_sign_in()}
{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}
diff --git a/frontend/src/routes/login/alternative/code/+page.svelte b/frontend/src/routes/login/alternative/code/+page.svelte
index aa3236e9..59e4bfc9 100644
--- a/frontend/src/routes/login/alternative/code/+page.svelte
+++ b/frontend/src/routes/login/alternative/code/+page.svelte
@@ -59,7 +59,7 @@
-
{m.login_code()}
+
{m.login_code()}
{#if error}
{error}. {m.please_try_again()}
diff --git a/frontend/src/routes/login/alternative/email/+page.svelte b/frontend/src/routes/login/alternative/email/+page.svelte
index 3cef4135..e1718fcd 100644
--- a/frontend/src/routes/login/alternative/email/+page.svelte
+++ b/frontend/src/routes/login/alternative/email/+page.svelte
@@ -5,9 +5,9 @@
import Input from '$lib/components/ui/input/input.svelte';
import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service';
+ import { preventDefault } from '$lib/utils/event-util';
import { fade } from 'svelte/transition';
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
- import { preventDefault } from '$lib/utils/event-util';
const { data } = $props();
@@ -37,7 +37,7 @@
-
{m.email_login()}
+
{m.email_login()}
{#if error}
{error}. {m.please_try_again()}
diff --git a/frontend/src/routes/logout/+page.svelte b/frontend/src/routes/logout/+page.svelte
index d8792831..b95d2c0b 100644
--- a/frontend/src/routes/logout/+page.svelte
+++ b/frontend/src/routes/logout/+page.svelte
@@ -34,7 +34,7 @@
-
{m.sign_out()}
+
{m.sign_out()}
-
-
-
- {m.settings()}
-
-
-
-
+
{@render children()}
diff --git a/frontend/src/routes/settings/account/+page.svelte b/frontend/src/routes/settings/account/+page.svelte
index 4c3b91a0..6889a998 100644
--- a/frontend/src/routes/settings/account/+page.svelte
+++ b/frontend/src/routes/settings/account/+page.svelte
@@ -141,7 +141,7 @@
-
+
diff --git a/frontend/src/routes/settings/account/login-code-modal.svelte b/frontend/src/routes/settings/account/login-code-modal.svelte
index 709a047a..20437492 100644
--- a/frontend/src/routes/settings/account/login-code-modal.svelte
+++ b/frontend/src/routes/settings/account/login-code-modal.svelte
@@ -64,7 +64,6 @@
value={loginCodeLink}
size={150}
color={mode.current === 'dark' ? '#FFFFFF' : '#000000'}
- backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'}
/>
{loginCodeLink!}
diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte
index f888a281..b3bdc1c3 100644
--- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte
+++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte
@@ -115,7 +115,7 @@
{/if}
-
+
{m.users()}
-
+
diff --git a/frontend/src/routes/signup/+page.svelte b/frontend/src/routes/signup/+page.svelte
index 41747516..094fcc3b 100644
--- a/frontend/src/routes/signup/+page.svelte
+++ b/frontend/src/routes/signup/+page.svelte
@@ -61,7 +61,7 @@
-
+
{m.signup_to_appname({ appName: $appConfigStore.appName })}
diff --git a/frontend/src/routes/signup/add-passkey/+page.svelte b/frontend/src/routes/signup/add-passkey/+page.svelte
index d30f7d43..f8369169 100644
--- a/frontend/src/routes/signup/add-passkey/+page.svelte
+++ b/frontend/src/routes/signup/add-passkey/+page.svelte
@@ -69,7 +69,7 @@
-
+
{m.setup_your_passkey()}
diff --git a/frontend/src/routes/signup/setup/+page.svelte b/frontend/src/routes/signup/setup/+page.svelte
index 78c16739..fb9bc1d5 100644
--- a/frontend/src/routes/signup/setup/+page.svelte
+++ b/frontend/src/routes/signup/setup/+page.svelte
@@ -47,7 +47,7 @@
-
+
{m.signup_to_appname({ appName: $appConfigStore.appName })}
diff --git a/frontend/static/fonts/PlayfairDisplay-Bold.woff b/frontend/static/fonts/PlayfairDisplay-Bold.woff
deleted file mode 100644
index 3e4f6da7..00000000
Binary files a/frontend/static/fonts/PlayfairDisplay-Bold.woff and /dev/null differ
diff --git a/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff b/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff
deleted file mode 100644
index a42fd641..00000000
Binary files a/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff and /dev/null differ
diff --git a/frontend/static/fonts/PlayfairDisplay-Medium.woff b/frontend/static/fonts/PlayfairDisplay-Medium.woff
deleted file mode 100644
index 8c11b8d5..00000000
Binary files a/frontend/static/fonts/PlayfairDisplay-Medium.woff and /dev/null differ
diff --git a/frontend/static/fonts/PlayfairDisplay-Regular.woff b/frontend/static/fonts/PlayfairDisplay-Regular.woff
deleted file mode 100644
index 688623bc..00000000
Binary files a/frontend/static/fonts/PlayfairDisplay-Regular.woff and /dev/null differ
diff --git a/frontend/static/fonts/PlayfairDisplay-SemiBold.woff b/frontend/static/fonts/PlayfairDisplay-SemiBold.woff
deleted file mode 100644
index 76a30f32..00000000
Binary files a/frontend/static/fonts/PlayfairDisplay-SemiBold.woff and /dev/null differ
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 244fc94a..9b565ee9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -61,6 +61,9 @@ importers:
frontend:
dependencies:
+ '@fontsource/gloock':
+ specifier: ^5.2.8
+ version: 5.2.8
'@simplewebauthn/browser':
specifier: ^13.3.0
version: 13.3.0
@@ -633,6 +636,9 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@fontsource/gloock@5.2.8':
+ resolution: {integrity: sha512-2qh1h3uC8bY93r2zuX3a0Q2IoGUf4vc1qxM0DUqoNVxwqDkXIOhGS7/0EEYNBLfSWO7ABfi/qkVmzedkj2jSZA==}
+
'@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -3583,6 +3589,8 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@fontsource/gloock@5.2.8': {}
+
'@hapi/hoek@9.3.0':
optional: true
diff --git a/tests/resources/export/uploads/profile-pictures/defaults/CF.png b/tests/resources/export/uploads/profile-pictures/defaults/CF.png
index 7c399f55..b608b8f7 100644
Binary files a/tests/resources/export/uploads/profile-pictures/defaults/CF.png and b/tests/resources/export/uploads/profile-pictures/defaults/CF.png differ
diff --git a/tests/resources/export/uploads/profile-pictures/defaults/TC.png b/tests/resources/export/uploads/profile-pictures/defaults/TC.png
index b8192304..316c10b3 100644
Binary files a/tests/resources/export/uploads/profile-pictures/defaults/TC.png and b/tests/resources/export/uploads/profile-pictures/defaults/TC.png differ
diff --git a/tests/specs/application-configuration.spec.ts b/tests/specs/application-configuration.spec.ts
index 570df386..73aa1179 100644
--- a/tests/specs/application-configuration.spec.ts
+++ b/tests/specs/application-configuration.spec.ts
@@ -18,14 +18,13 @@ test('Update general configuration', async ({ page }) => {
await expect(page.locator('[data-type="success"]')).toHaveText(
'Application configuration updated successfully'
);
- await expect(page.getByTestId('application-name')).toHaveText('Updated Name');
await page.reload();
await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue('Updated Name');
await expect(page.getByLabel('Session Duration')).toHaveValue('30');
- await page.getByRole('link', { name: 'Logo Updated Name' }).click();
+ await page.getByRole('link', { name: 'Logo' }).click();
await page.waitForURL('/settings/apps');
});
diff --git a/tests/specs/cli.spec.ts b/tests/specs/cli.spec.ts
index 8b1733f8..0e6e7234 100644
--- a/tests/specs/cli.spec.ts
+++ b/tests/specs/cli.spec.ts
@@ -109,10 +109,16 @@ function compareExports(dir1: string, dir2: string): void {
const hashes2 = hashAllFiles(dir2);
const files1 = Object.keys(hashes1).sort();
- const files2 = Object.keys(hashes2).sort().filter(p => !p.includes('.inited'));
+ const files2 = Object.keys(hashes2)
+ .sort()
+ .filter((p) => !p.includes('.inited'));
expect(files2).toEqual(files1);
for (const file of files1) {
+ const profilePictures = ['CF.png', 'CR.png'];
+ if (profilePictures.includes(path.basename(file))) {
+ continue; // Skip profile pictures because they get generated on demand and can differ between exports
+ }
expect(hashes2[file], `${file} hash should match`).toEqual(hashes1[file]);
}
diff --git a/tests/specs/oidc.spec.ts b/tests/specs/oidc.spec.ts
index 825bc947..066ebfd8 100644
--- a/tests/specs/oidc.spec.ts
+++ b/tests/specs/oidc.spec.ts
@@ -15,36 +15,21 @@ test.beforeEach(async () => await cleanupBackend());
test('Authorize existing client', async ({ page }) => {
const oidcClient = oidcClients.nextcloud;
const urlParams = createUrlParams(oidcClient);
- await page.goto(`/authorize?${urlParams.toString()}`);
-
- // Ignore DNS resolution error as the callback URL is not reachable
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.goto(`/authorize?${urlParams.toString()}`)
+ );
});
test('Authorize existing client while not signed in', async ({ page }) => {
const oidcClient = oidcClients.nextcloud;
const urlParams = createUrlParams(oidcClient);
await page.context().clearCookies();
- await page.goto(`/authorize?${urlParams.toString()}`);
- await (await passkeyUtil.init(page)).addPasskey();
- await page.getByRole('button', { name: 'Sign in' }).click();
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, async () => {
+ await page.goto(`/authorize?${urlParams.toString()}`);
- // Ignore DNS resolution error as the callback URL is not reachable
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
+ await (await passkeyUtil.init(page)).addPasskey();
+ await page.getByRole('button', { name: 'Sign in' }).click();
});
});
@@ -56,17 +41,9 @@ test('Authorize new client', async ({ page }) => {
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible();
- await page.getByRole('button', { name: 'Sign in' }).click();
-
- // Ignore DNS resolution error as the callback URL is not reachable
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.getByRole('button', { name: 'Sign in' }).click()
+ );
});
test('Authorize new client while not signed in', async ({ page }) => {
@@ -81,17 +58,9 @@ test('Authorize new client while not signed in', async ({ page }) => {
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible();
- await page.getByRole('button', { name: 'Sign in' }).click();
-
- // Ignore DNS resolution error as the callback URL is not reachable
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.getByRole('button', { name: 'Sign in' }).click()
+ );
});
test('Authorize new client fails with user group not allowed', async ({ page }) => {
@@ -140,23 +109,11 @@ test('End session with id token hint redirects to callback URL', async ({ page }
users.tim,
client.id
);
- let redirectedCorrectly = false;
- await page
- .goto(
+ await expectCallbackRedirect(page, client.logoutCallbackUrl, () =>
+ page.goto(
`/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}`
)
- .catch((e) => {
- if (
- e.message.includes('net::ERR_NAME_NOT_RESOLVED') ||
- e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- redirectedCorrectly = true;
- } else {
- throw e;
- }
- });
-
- expect(redirectedCorrectly).toBeTruthy();
+ );
});
test('Successfully refresh tokens with valid refresh token', async ({ request }) => {
@@ -681,17 +638,9 @@ test('Forces reauthentication when client requires it', async ({ page, request }
await (await passkeyUtil.init(page)).addPasskey();
const urlParams = createUrlParams(oidcClients.nextcloud);
- await page.goto(`/authorize?${urlParams.toString()}`);
-
- await expect(page.getByTestId('scopes')).not.toBeVisible();
-
- await page.waitForURL(oidcClients.nextcloud.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
+ await expectCallbackRedirect(page, oidcClients.nextcloud.callbackUrl, async () => {
+ await page.goto(`/authorize?${urlParams.toString()}`);
+ await expect(page.getByTestId('scopes')).not.toBeVisible();
});
expect(webauthnStartCalled).toBe(true);
@@ -725,6 +674,22 @@ test('Authorize existing client with response_mode=form_post', async ({ page })
await expectFormPostRequest(formPostRequestPromise);
});
+test('Authorize existing client with response_mode=fragment', async ({ page }) => {
+ const oidcClient = oidcClients.nextcloud;
+ const urlParams = createUrlParams(oidcClient);
+ urlParams.set('response_mode', 'fragment');
+
+ const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.goto(`/authorize?${urlParams.toString()}`)
+ );
+ expect(redirectUrl.search).toBe('');
+
+ const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1));
+ expect(fragmentParams.get('code')).toBeTruthy();
+ expect(fragmentParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
+ expect(fragmentParams.get('iss')).toBeTruthy();
+});
+
function waitForFormPostRequest(page: Page, callbackUrl: string): Promise {
return page.waitForRequest(
(request) => request.method() === 'POST' && request.url() === callbackUrl
@@ -758,6 +723,23 @@ test.describe('OIDC prompt parameter', () => {
expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
});
+ test('prompt=none redirects errors with response_mode=fragment', async ({ page }) => {
+ await page.context().clearCookies();
+ const oidcClient = oidcClients.nextcloud;
+ const urlParams = createUrlParams(oidcClient);
+ urlParams.set('prompt', 'none');
+ urlParams.set('response_mode', 'fragment');
+
+ const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.goto(`/authorize?${urlParams.toString()}`)
+ );
+ expect(redirectUrl.search).toBe('');
+
+ const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1));
+ expect(fragmentParams.get('error')).toBe('login_required');
+ expect(fragmentParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
+ });
+
test('prompt=none redirects with consent_required when authorization needed', async ({
page
}) => {
@@ -779,17 +761,9 @@ test.describe('OIDC prompt parameter', () => {
const urlParams = createUrlParams(oidcClient);
urlParams.set('prompt', 'none');
- await page.goto(`/authorize?${urlParams.toString()}`);
-
- // Should redirect successfully to callback URL with code
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.goto(`/authorize?${urlParams.toString()}`)
+ );
});
test('prompt=consent forces consent display even for authorized client', async ({ page }) => {
@@ -805,17 +779,9 @@ test.describe('OIDC prompt parameter', () => {
).toBeVisible();
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
- await page.getByRole('button', { name: 'Sign in' }).click();
-
- // Should redirect successfully after consent
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.getByRole('button', { name: 'Sign in' }).click()
+ );
});
test('prompt=login forces reauthentication', async ({ page }) => {
@@ -830,17 +796,9 @@ test.describe('OIDC prompt parameter', () => {
});
await (await passkeyUtil.init(page)).addPasskey();
- await page.goto(`/authorize?${urlParams.toString()}`);
-
- // Should require reauthentication even though user is signed in
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.goto(`/authorize?${urlParams.toString()}`)
+ );
expect(reauthCalled).toBe(true);
});
@@ -857,17 +815,9 @@ test.describe('OIDC prompt parameter', () => {
await expect(selectionCard).toBeVisible();
await expect(selectionCard).toContainText('Tim Cook');
- await page.getByRole('button', { name: 'Sign In' }).click();
-
- // Should redirect successfully to callback URL with code
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.getByRole('button', { name: 'Sign In' }).click()
+ );
});
test('prompt=select_account account can be changed', async ({ page }) => {
@@ -884,17 +834,9 @@ test.describe('OIDC prompt parameter', () => {
await page.getByRole('button', { name: 'Sign In' }).click();
- await page.getByRole('button', { name: 'Sign In' }).click();
-
- // Should redirect successfully to callback URL with code
- await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
- if (
- !e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
- !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
- ) {
- throw e;
- }
- });
+ await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
+ page.getByRole('button', { name: 'Sign In' }).click()
+ );
});
test('prompt=none with prompt=consent returns interaction_required', async ({ page }) => {
@@ -939,3 +881,58 @@ test.describe('OIDC prompt parameter', () => {
expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
});
});
+
+async function waitForCallbackURL(page: Page, callbackUrl: string): Promise {
+ const expectedUrl = new URL(callbackUrl);
+ const isCallbackURL = (url: URL) =>
+ url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname;
+
+ const callbackRequest = await page.waitForRequest((request) =>
+ isCallbackURL(new URL(request.url()))
+ );
+ await page.waitForURL(isCallbackURL, { waitUntil: 'commit' }).catch(() => {});
+
+ const currentURL = new URL(page.url());
+ if (isCallbackURL(currentURL)) {
+ return currentURL;
+ }
+
+ return new URL(callbackRequest.url());
+}
+
+async function expectCallbackRedirect(
+ page: Page,
+ callbackUrl: string,
+ action: () => Promise
+): Promise {
+ const callbackRouteMatcher = await routeCallbackPage(page, callbackUrl);
+
+ try {
+ const callbackURLPromise = waitForCallbackURL(page, callbackUrl);
+ const actionPromise = action().then(
+ () => undefined,
+ (error) => error
+ );
+ const callbackURL = await callbackURLPromise;
+ await actionPromise;
+ return callbackURL;
+ } finally {
+ await page.unroute(callbackRouteMatcher);
+ }
+}
+
+async function routeCallbackPage(page: Page, callbackUrl: string): Promise<(url: URL) => boolean> {
+ const expectedUrl = new URL(callbackUrl);
+ const callbackRouteMatcher = (url: URL) =>
+ url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname;
+
+ await page.route(callbackRouteMatcher, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'text/html',
+ body: 'OIDC callback'
+ });
+ });
+
+ return callbackRouteMatcher;
+}