From 2f1451b0fe0529e15407770cbad52ad100048f50 Mon Sep 17 00:00:00 2001 From: seanperez Date: Wed, 29 Jul 2026 16:01:34 -0400 Subject: [PATCH 1/3] feat(user): add trusted device management --- trusted_device.go | 17 +++++++++++++++ user/api.go | 10 +++++++++ user/client.go | 33 ++++++++++++++++++++++++++++++ user/client_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 trusted_device.go diff --git a/trusted_device.go b/trusted_device.go new file mode 100644 index 00000000..c203a9c6 --- /dev/null +++ b/trusted_device.go @@ -0,0 +1,17 @@ +package clerk + +// TrustedDevice represents a device enrolled by a user for trusted-device authentication. +type TrustedDevice struct { + APIResource + ID string `json:"id"` + Object string `json:"object"` + Platform string `json:"platform"` + AppIdentifier string `json:"app_identifier"` + Name *string `json:"name"` + Algorithm string `json:"algorithm"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + LastUsedAt *int64 `json:"last_used_at"` + RevokedAt *int64 `json:"revoked_at"` +} diff --git a/user/api.go b/user/api.go index 09b9ff16..0b2ab70b 100644 --- a/user/api.go +++ b/user/api.go @@ -115,6 +115,16 @@ func DeletePasskey(ctx context.Context, userID, identificationID string) (*clerk return getClient().DeletePasskey(ctx, userID, identificationID) } +// ListTrustedDevices lists a user's active trusted devices. +func ListTrustedDevices(ctx context.Context, userID string) ([]*clerk.TrustedDevice, error) { + return getClient().ListTrustedDevices(ctx, userID) +} + +// RevokeTrustedDevice revokes a user's trusted device. +func RevokeTrustedDevice(ctx context.Context, userID, trustedDeviceID string) (*clerk.TrustedDevice, error) { + return getClient().RevokeTrustedDevice(ctx, userID, trustedDeviceID) +} + // DeleteWeb3Wallet deletes a web3 wallet by its identification ID. func DeleteWeb3Wallet(ctx context.Context, userID, identificationID string) (*clerk.DeletedResource, error) { return getClient().DeleteWeb3Wallet(ctx, userID, identificationID) diff --git a/user/client.go b/user/client.go index 7a07e531..9355b7d6 100644 --- a/user/client.go +++ b/user/client.go @@ -571,6 +571,39 @@ func (c *Client) DeletePasskey(ctx context.Context, userID, identificationID str return resource, err } +// ListTrustedDevices lists a user's active trusted devices. +func (c *Client) ListTrustedDevices(ctx context.Context, userID string) ([]*clerk.TrustedDevice, error) { + path, err := clerk.JoinPath(path, userID, "/trusted_devices") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + resource := &trustedDeviceList{} + err = c.Backend.Call(ctx, req, resource) + return []*clerk.TrustedDevice(*resource), err +} + +// Custom type needed in order to store the GET /v1/users/{userID}/trusted_devices +// response array. +type trustedDeviceList []*clerk.TrustedDevice + +// Read implements the clerk.ResponseReader interface. +func (_ *trustedDeviceList) Read(_ *clerk.APIResponse) { + // no-op +} + +// RevokeTrustedDevice revokes a user's trusted device. +func (c *Client) RevokeTrustedDevice(ctx context.Context, userID, trustedDeviceID string) (*clerk.TrustedDevice, error) { + path, err := clerk.JoinPath(path, userID, "/trusted_devices", trustedDeviceID) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + resource := &clerk.TrustedDevice{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + // DeleteWeb3Wallet deletes a web3 wallet by its identification ID. func (c *Client) DeleteWeb3Wallet(ctx context.Context, userID, identificationID string) (*clerk.DeletedResource, error) { path, err := clerk.JoinPath(path, userID, "/web3_wallets", identificationID) diff --git a/user/client_test.go b/user/client_test.go index 5877ff2c..67657b1a 100644 --- a/user/client_test.go +++ b/user/client_test.go @@ -601,6 +601,56 @@ func TestUserClientDeletePasskey(t *testing.T) { require.NoError(t, err) require.Equal(t, passkeyIdentificationID, passkey.ID) } + +func TestUserClientListTrustedDevices(t *testing.T) { + t.Parallel() + userID := "user_123" + trustedDeviceID := "tdc_345" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`[{"id":"%s","object":"trusted_device","platform":"ios","app_identifier":"com.example.app","algorithm":"ES256","status":"active","created_at":1,"updated_at":2}]`, trustedDeviceID)), + Method: http.MethodGet, + Path: "/v1/users/" + userID + "/trusted_devices", + }, + } + client := NewClient(config) + trustedDevices, err := client.ListTrustedDevices(context.Background(), userID) + require.NoError(t, err) + require.Len(t, trustedDevices, 1) + require.Equal(t, trustedDeviceID, trustedDevices[0].ID) + require.Equal(t, "trusted_device", trustedDevices[0].Object) + require.Equal(t, "ios", trustedDevices[0].Platform) + require.Equal(t, "com.example.app", trustedDevices[0].AppIdentifier) + require.Equal(t, "ES256", trustedDevices[0].Algorithm) + require.Equal(t, "active", trustedDevices[0].Status) + require.Equal(t, int64(1), trustedDevices[0].CreatedAt) + require.Equal(t, int64(2), trustedDevices[0].UpdatedAt) +} + +func TestUserClientRevokeTrustedDevice(t *testing.T) { + t.Parallel() + userID := "user_123" + trustedDeviceID := "tdc_345" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","object":"trusted_device","platform":"ios","app_identifier":"com.example.app","algorithm":"ES256","status":"revoked","created_at":1,"updated_at":2,"revoked_at":2}`, trustedDeviceID)), + Method: http.MethodDelete, + Path: "/v1/users/" + userID + "/trusted_devices/" + trustedDeviceID, + }, + } + client := NewClient(config) + trustedDevice, err := client.RevokeTrustedDevice(context.Background(), userID, trustedDeviceID) + require.NoError(t, err) + require.Equal(t, trustedDeviceID, trustedDevice.ID) + require.Equal(t, "revoked", trustedDevice.Status) + require.NotNil(t, trustedDevice.RevokedAt) + require.Equal(t, int64(2), *trustedDevice.RevokedAt) +} + func TestUserClientDeleteWeb3Wallet(t *testing.T) { t.Parallel() userID := "user_123" From 26b785536d903945a4e9382e21f70b2e66da59e0 Mon Sep 17 00:00:00 2001 From: seanperez Date: Thu, 30 Jul 2026 09:36:13 -0400 Subject: [PATCH 2/3] fix(user): return trusted device list resource --- trusted_device.go | 7 +++++++ user/api.go | 2 +- user/client.go | 13 ++++++++++--- user/client_test.go | 19 ++++++++++--------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/trusted_device.go b/trusted_device.go index c203a9c6..fdbabe97 100644 --- a/trusted_device.go +++ b/trusted_device.go @@ -15,3 +15,10 @@ type TrustedDevice struct { LastUsedAt *int64 `json:"last_used_at"` RevokedAt *int64 `json:"revoked_at"` } + +// TrustedDeviceList represents a list of a user's active trusted devices. +type TrustedDeviceList struct { + APIResource + TrustedDevices []*TrustedDevice `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/user/api.go b/user/api.go index 0b2ab70b..468b2eb5 100644 --- a/user/api.go +++ b/user/api.go @@ -116,7 +116,7 @@ func DeletePasskey(ctx context.Context, userID, identificationID string) (*clerk } // ListTrustedDevices lists a user's active trusted devices. -func ListTrustedDevices(ctx context.Context, userID string) ([]*clerk.TrustedDevice, error) { +func ListTrustedDevices(ctx context.Context, userID string) (*clerk.TrustedDeviceList, error) { return getClient().ListTrustedDevices(ctx, userID) } diff --git a/user/client.go b/user/client.go index 9355b7d6..3f9bed7d 100644 --- a/user/client.go +++ b/user/client.go @@ -572,15 +572,22 @@ func (c *Client) DeletePasskey(ctx context.Context, userID, identificationID str } // ListTrustedDevices lists a user's active trusted devices. -func (c *Client) ListTrustedDevices(ctx context.Context, userID string) ([]*clerk.TrustedDevice, error) { +func (c *Client) ListTrustedDevices(ctx context.Context, userID string) (*clerk.TrustedDeviceList, error) { path, err := clerk.JoinPath(path, userID, "/trusted_devices") if err != nil { return nil, err } req := clerk.NewAPIRequest(http.MethodGet, path) resource := &trustedDeviceList{} - err = c.Backend.Call(ctx, req, resource) - return []*clerk.TrustedDevice(*resource), err + if err = c.Backend.Call(ctx, req, resource); err != nil { + return nil, err + } + + trustedDevices := []*clerk.TrustedDevice(*resource) + return &clerk.TrustedDeviceList{ + TrustedDevices: trustedDevices, + TotalCount: int64(len(trustedDevices)), + }, nil } // Custom type needed in order to store the GET /v1/users/{userID}/trusted_devices diff --git a/user/client_test.go b/user/client_test.go index 67657b1a..f172c438 100644 --- a/user/client_test.go +++ b/user/client_test.go @@ -618,15 +618,16 @@ func TestUserClientListTrustedDevices(t *testing.T) { client := NewClient(config) trustedDevices, err := client.ListTrustedDevices(context.Background(), userID) require.NoError(t, err) - require.Len(t, trustedDevices, 1) - require.Equal(t, trustedDeviceID, trustedDevices[0].ID) - require.Equal(t, "trusted_device", trustedDevices[0].Object) - require.Equal(t, "ios", trustedDevices[0].Platform) - require.Equal(t, "com.example.app", trustedDevices[0].AppIdentifier) - require.Equal(t, "ES256", trustedDevices[0].Algorithm) - require.Equal(t, "active", trustedDevices[0].Status) - require.Equal(t, int64(1), trustedDevices[0].CreatedAt) - require.Equal(t, int64(2), trustedDevices[0].UpdatedAt) + require.Equal(t, int64(1), trustedDevices.TotalCount) + require.Len(t, trustedDevices.TrustedDevices, 1) + require.Equal(t, trustedDeviceID, trustedDevices.TrustedDevices[0].ID) + require.Equal(t, "trusted_device", trustedDevices.TrustedDevices[0].Object) + require.Equal(t, "ios", trustedDevices.TrustedDevices[0].Platform) + require.Equal(t, "com.example.app", trustedDevices.TrustedDevices[0].AppIdentifier) + require.Equal(t, "ES256", trustedDevices.TrustedDevices[0].Algorithm) + require.Equal(t, "active", trustedDevices.TrustedDevices[0].Status) + require.Equal(t, int64(1), trustedDevices.TrustedDevices[0].CreatedAt) + require.Equal(t, int64(2), trustedDevices.TrustedDevices[0].UpdatedAt) } func TestUserClientRevokeTrustedDevice(t *testing.T) { From 26fbb3d75e40cd9c14e78f38b6d5dcf52f405d49 Mon Sep 17 00:00:00 2001 From: seanperez Date: Thu, 30 Jul 2026 11:53:40 -0400 Subject: [PATCH 3/3] fix(user): decode trusted device list resource --- user/client.go | 22 +++------------------- user/client_test.go | 2 +- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/user/client.go b/user/client.go index 3f9bed7d..890ec097 100644 --- a/user/client.go +++ b/user/client.go @@ -578,25 +578,9 @@ func (c *Client) ListTrustedDevices(ctx context.Context, userID string) (*clerk. return nil, err } req := clerk.NewAPIRequest(http.MethodGet, path) - resource := &trustedDeviceList{} - if err = c.Backend.Call(ctx, req, resource); err != nil { - return nil, err - } - - trustedDevices := []*clerk.TrustedDevice(*resource) - return &clerk.TrustedDeviceList{ - TrustedDevices: trustedDevices, - TotalCount: int64(len(trustedDevices)), - }, nil -} - -// Custom type needed in order to store the GET /v1/users/{userID}/trusted_devices -// response array. -type trustedDeviceList []*clerk.TrustedDevice - -// Read implements the clerk.ResponseReader interface. -func (_ *trustedDeviceList) Read(_ *clerk.APIResponse) { - // no-op + resource := &clerk.TrustedDeviceList{} + err = c.Backend.Call(ctx, req, resource) + return resource, err } // RevokeTrustedDevice revokes a user's trusted device. diff --git a/user/client_test.go b/user/client_test.go index f172c438..01640cdf 100644 --- a/user/client_test.go +++ b/user/client_test.go @@ -610,7 +610,7 @@ func TestUserClientListTrustedDevices(t *testing.T) { config.HTTPClient = &http.Client{ Transport: &clerktest.RoundTripper{ T: t, - Out: json.RawMessage(fmt.Sprintf(`[{"id":"%s","object":"trusted_device","platform":"ios","app_identifier":"com.example.app","algorithm":"ES256","status":"active","created_at":1,"updated_at":2}]`, trustedDeviceID)), + Out: json.RawMessage(fmt.Sprintf(`{"data":[{"id":"%s","object":"trusted_device","platform":"ios","app_identifier":"com.example.app","algorithm":"ES256","status":"active","created_at":1,"updated_at":2}],"total_count":1}`, trustedDeviceID)), Method: http.MethodGet, Path: "/v1/users/" + userID + "/trusted_devices", },