Repository: AthenZ/athenz

Issue:
Implementing Caching Functionality for Golang ZTSClient
I would like to consult with you about this matter.
https://github.com/AthenZ/athenz/pull/2879

We have a team within our company that is using the Golang client library. Currently, the Golang ZTSClient does not have a caching function, which is causing spikes to occur with ZTS. We are considering providing a ZTSClient equipped with a caching function.

I understand that the current clients/go/zts/client.go is code automatically generated from RDL. If we modify this code, it will be overwritten when client.go is regenerated from RDL.

As a proof of concept, we have submitted a pull request for a client with caching capabilities. If possible, I think it would be better to provide a client.go that supports caching while maintaining backward compatibility.

Could you please provide advice on the file placement and which files to modify in order to provide a Golang ZTSClient with caching capabilities?

Discussion:
@havetisyan: @ysknkd looking at the code - you're not necessarily providing a zts client replacement - the zts client you have only provides access to role and access tokens and not necessarily all the api endpoints that zts server supports. So functionality wise it's more like a ZTSTokenClient and not just ZTSClient. Since those are probably the most commonly used methods anyway with ZTS, it's a great improvement to what we currently have (btw, we typically utilize SIA to fetch and refresh tokens on disk). So thanks for the submission.

So a couple of questions about the PR:
a) what was the idea behind tenant directory name? From users looking at this they might not be clear what tenant means? Why not call the directory as token or maybe even cache to clarify the functionality it provides?
b) What is the purpose of the zts_client_wrapper.go?
c) for the response objects - why define new AccessToken and RoleToken and not use AccessTokenResponse and RoleToken objects from ZTS? the access token api returns more than just access token - it can return id token, scope, etc.@TakuyaMatsu: @havetisyan 
thank you. I will answer the questions.

a) It’s true that “tenant” can be confusing, so I’ll update it.
b) Since the code generated by RDL doesn’t provide an interface, I can’t mock it. so I created a wrapper to make the code testable.
c) I were returning only the token for simplicity. However, considering that scope and other fields might be used, I’ll revise it to return ZTS’s AccessTokenResponse and RoleToken.@TakuyaMatsu: Similar functionality has been implemented in pull request #2892 by adding to the ztsroletoken package under libs/go. However, the ztsroletoken package is intended for the zts-roletoken client utility. Therefore, I believe implementing a prefetch feature in ztsroletoken is not the correct approach. Is my understanding correct?@ysknkd: @havetisyan Thanks a lot for taking a look at and merging #2879. Just wanted to let you know that we've got another PR, #2892, up with a similar feature. Some parts seem to overlap with what we've implemented in token/role_token.go, but there are also some unique bits in this new PR.

As we think about improving the token retrieval in Golang down the line, I'm thinking it might be a good idea to make our changes in token/role_token.go. Would love to hear what you think!@havetisyan: @ysknkd @TakuyaMatsu @massakam 

So both the PRs were merged. However, I do agree with @TakuyaMatsu that we would prefer to add the functionality all in one client. So if the zts client needs to have prefetch capabilities in go, the appropriate plan would be to update the zts client in https://github.com/AthenZ/athenz/tree/master/clients/go/zts/token and not continue use that other role token module. For backward compatibility reasons we won't be removing the roletoken library, but I would strongly recommend using the zts client directly.

As a side node, I would strongly discourage the use of role tokens as well. If possible, please plan to move to use standards based jwt access tokens instead of role tokens.@ysknkd: Thank you very much for taking the time to review this. I close this issue.

Solution patch:
diff --git a/CHANGELOG b/CHANGELOG
index a16b7f586..fbec83b3d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,20 @@
+Athenz v1.12.11 Release / Feb 21, 2025
+--------------------------------------
+
+#2874 update go image to 1.23 for docker builds
+#2875 extract access token request body into its own class
+#2877 Resource ownership override support using a special keyword
+#2878 support client_assertion with jwt bearer token
+#2880 UI: update dependencies
+#2881 Rename TokenRequest classes to TokenScope for correct representation
+#2882 rename request->scope objects + test classes
+#2883 Update slack handle error logic & handle null in PendingRoleMembershipTask
+#2884 rename body->request class name for correct representation
+#2885 expose client assertion parameter for access token call
+#2886 fix minor typos and Go imports order
+#2887 UI: functional test - fix slack channel test
+#2889 update java and go dependencies to their latest releases
+
 Athenz v1.12.10 Release / Feb 07, 2025
 --------------------------------------
 
diff --git a/assembly/ui/pom.xml b/assembly/ui/pom.xml
index 86afa84b0..0d8e42fac 100644
--- a/assembly/ui/pom.xml
+++ b/assembly/ui/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/assembly/utils/pom.xml b/assembly/utils/pom.xml
index e2d44b505..02fafab35 100644
--- a/assembly/utils/pom.xml
+++ b/assembly/utils/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/assembly/zms/pom.xml b/assembly/zms/pom.xml
index 4e92386e9..320837b7b 100644
--- a/assembly/zms/pom.xml
+++ b/assembly/zms/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/assembly/zts/pom.xml b/assembly/zts/pom.xml
index afe33213b..171ebaa25 100644
--- a/assembly/zts/pom.xml
+++ b/assembly/zts/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml
index 03447fc10..1f4d4e34f 100644
--- a/checkstyle-suppressions.xml
+++ b/checkstyle-suppressions.xml
@@ -29,6 +29,12 @@
   <suppress checks="MethodName"
             files="OAuthConfig.java"
             lines="1-9999"/>
+  <suppress checks="MemberName"
+            files="IntrospectResponse.java"
+            lines="1-9999"/>
+  <suppress checks="MethodName"
+            files="IntrospectResponse.java"
+            lines="1-9999"/>
   <suppress checks="MemberName"
             files="OIDCResponse.java"
             lines="1-9999"/>
diff --git a/clients/go/msd/pom.xml b/clients/go/msd/pom.xml
index cef76431b..f97479a04 100644
--- a/clients/go/msd/pom.xml
+++ b/clients/go/msd/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/go/zms/pom.xml b/clients/go/zms/pom.xml
index d0bf72baf..a8f978959 100644
--- a/clients/go/zms/pom.xml
+++ b/clients/go/zms/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/go/zts/client.go b/clients/go/zts/client.go
index aaa831de0..89a939d0f 100644
--- a/clients/go/zts/client.go
+++ b/clients/go/zts/client.go
@@ -1192,6 +1192,39 @@ func (client ZTSClient) GetOIDCResponse(responseType string, clientId ServiceNam
 	}
 }
 
+func (client ZTSClient) PostIntrospectRequest(request IntrospectRequest) (*IntrospectResponse, error) {
+	var data *IntrospectResponse
+	url := client.URL + "/oauth2/introspect"
+	contentBytes := []byte(request)
+	resp, err := client.httpPostWithContentType(url, nil, contentBytes, "application/x-www-form-urlencoded")
+	if err != nil {
+		return data, err
+	}
+	defer resp.Body.Close()
+	switch resp.StatusCode {
+	case 200:
+		err = json.NewDecoder(resp.Body).Decode(&data)
+		if err != nil {
+			return data, err
+		}
+		return data, nil
+	default:
+		var errobj rdl.ResourceError
+		contentBytes, err = io.ReadAll(resp.Body)
+		if err != nil {
+			return data, err
+		}
+		json.Unmarshal(contentBytes, &errobj)
+		if errobj.Code == 0 {
+			errobj.Code = resp.StatusCode
+		}
+		if errobj.Message == "" {
+			errobj.Message = string(contentBytes)
+		}
+		return data, errobj
+	}
+}
+
 func (client ZTSClient) PostRoleCertificateRequestExt(req *RoleCertificateRequest) (*RoleCertificate, error) {
 	var data *RoleCertificate
 	url := client.URL + "/rolecert"
diff --git a/clients/go/zts/examples/token/get-access-token/main.go b/clients/go/zts/examples/token/get-access-token/main.go
new file mode 100644
index 000000000..fde8e1261
--- /dev/null
+++ b/clients/go/zts/examples/token/get-access-token/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts/token"
+)
+
+func main() {
+	pem := "<path to client certificate>"
+	pk := "<path to private key>"
+	ctx := context.Background()
+	c, cancel := token.NewAccessTokenClientSetCacheUpdateDuration(ctx, "https://athenz.io", pem, pk, 3*time.Second)
+	defer cancel()
+	for i := 0; i < 10; i++ {
+		at, err := c.GetToken("user.provider.domain", []string{""})
+		if err != nil {
+			fmt.Printf("Error getting access token: %v\n", err)
+		} else {
+			fmt.Printf("AccessToken: %s\n", at.Token)
+			fmt.Printf("Expires_in: %d\n", at.ExpiryTime)
+		}
+		time.Sleep(1 * time.Second)
+	}
+}
diff --git a/clients/go/zts/examples/token/get-role-token/main.go b/clients/go/zts/examples/token/get-role-token/main.go
new file mode 100644
index 000000000..cea218368
--- /dev/null
+++ b/clients/go/zts/examples/token/get-role-token/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts/token"
+)
+
+func main() {
+	pem := "<path to client certificate>"
+	pk := "<path to private key>"
+	ctx := context.Background()
+	c, cancel := token.NewRoleTokenClientSetCacheUpdateDuration(ctx, "https://athenz.io", pem, pk, 3*time.Second)
+	defer cancel()
+	for i := 0; i < 10; i++ {
+		rt, err := c.GetToken("user.provider.domain", []string{""})
+		if err != nil {
+			fmt.Printf("Error getting role token: %v\n", err)
+		} else {
+			fmt.Printf("Role token: %s\n", rt.Token)
+		}
+		time.Sleep(1 * time.Second)
+	}
+
+}
diff --git a/clients/go/zts/model.go b/clients/go/zts/model.go
index 6533611db..bb599ed9e 100644
--- a/clients/go/zts/model.go
+++ b/clients/go/zts/model.go
@@ -2453,6 +2453,11 @@ type OAuthConfig struct {
 	//
 	Authorization_endpoint string `json:"authorization_endpoint"`
 
+	//
+	// oauth 2.0 introspection endpoint
+	//
+	Introspection_endpoint string `json:"introspection_endpoint"`
+
 	//
 	// authorization server token endpoint
 	//
@@ -2536,6 +2541,14 @@ func (self *OAuthConfig) Validate() error {
 			return fmt.Errorf("OAuthConfig.authorization_endpoint does not contain a valid String (%v)", val.Error)
 		}
 	}
+	if self.Introspection_endpoint == "" {
+		return fmt.Errorf("OAuthConfig.introspection_endpoint is missing but is a required field")
+	} else {
+		val := rdl.Validate(ZTSSchema(), "String", self.Introspection_endpoint)
+		if !val.Valid {
+			return fmt.Errorf("OAuthConfig.introspection_endpoint does not contain a valid String (%v)", val.Error)
+		}
+	}
 	if self.Token_endpoint == "" {
 		return fmt.Errorf("OAuthConfig.token_endpoint is missing but is a required field")
 	} else {
@@ -2692,6 +2705,167 @@ func (self *OIDCResponse) Validate() error {
 	return nil
 }
 
+// IntrospectRequest -
+type IntrospectRequest string
+
+// IntrospectResponse -
+type IntrospectResponse struct {
+
+	//
+	// token status
+	//
+	Active bool `json:"active"`
+
+	//
+	// version number
+	//
+	Ver int32 `json:"ver"`
+
+	//
+	// expiration time
+	//
+	Exp *int64 `json:"exp,omitempty" rdl:"optional"`
+
+	//
+	// issued at time
+	//
+	Iat *int64 `json:"iat,omitempty" rdl:"optional"`
+
+	//
+	// issued at time
+	//
+	Auth_time *int64 `json:"auth_time,omitempty" rdl:"optional"`
+
+	//
+	// audience
+	//
+	Aud string `json:"aud,omitempty" rdl:"optional"`
+
+	//
+	// issuer
+	//
+	Iss string `json:"iss,omitempty" rdl:"optional"`
+
+	//
+	// jwt id
+	//
+	Jti string `json:"jti,omitempty" rdl:"optional"`
+
+	//
+	// subject
+	//
+	Sub string `json:"sub,omitempty" rdl:"optional"`
+
+	//
+	// scope of the access token
+	//
+	Scope string `json:"scope,omitempty" rdl:"optional"`
+
+	//
+	// client id
+	//
+	Client_id string `json:"client_id,omitempty" rdl:"optional"`
+
+	//
+	// user id
+	//
+	Uid string `json:"uid,omitempty" rdl:"optional"`
+
+	//
+	// proxy principal
+	//
+	Proxy string `json:"proxy,omitempty" rdl:"optional"`
+
+	//
+	// authorization details
+	//
+	Authorization_details string `json:"authorization_details,omitempty" rdl:"optional"`
+}
+
+// NewIntrospectResponse - creates an initialized IntrospectResponse instance, returns a pointer to it
+func NewIntrospectResponse(init ...*IntrospectResponse) *IntrospectResponse {
+	var o *IntrospectResponse
+	if len(init) == 1 {
+		o = init[0]
+	} else {
+		o = new(IntrospectResponse)
+	}
+	return o
+}
+
+type rawIntrospectResponse IntrospectResponse
+
+// UnmarshalJSON is defined for proper JSON decoding of a IntrospectResponse
+func (self *IntrospectResponse) UnmarshalJSON(b []byte) error {
+	var m rawIntrospectResponse
+	err := json.Unmarshal(b, &m)
+	if err == nil {
+		o := IntrospectResponse(m)
+		*self = o
+		err = self.Validate()
+	}
+	return err
+}
+
+// Validate - checks for missing required fields, etc
+func (self *IntrospectResponse) Validate() error {
+	if self.Aud != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Aud)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.aud does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Iss != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Iss)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.iss does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Jti != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Jti)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.jti does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Sub != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Sub)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.sub does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Scope != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Scope)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.scope does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Client_id != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Client_id)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.client_id does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Uid != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Uid)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.uid does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Proxy != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Proxy)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.proxy does not contain a valid String (%v)", val.Error)
+		}
+	}
+	if self.Authorization_details != "" {
+		val := rdl.Validate(ZTSSchema(), "String", self.Authorization_details)
+		if !val.Valid {
+			return fmt.Errorf("IntrospectResponse.authorization_details does not contain a valid String (%v)", val.Error)
+		}
+	}
+	return nil
+}
+
 // InstanceRegisterInformation -
 type InstanceRegisterInformation struct {
 
diff --git a/clients/go/zts/pom.xml b/clients/go/zts/pom.xml
index 3eb05b9cc..f200ffe5f 100644
--- a/clients/go/zts/pom.xml
+++ b/clients/go/zts/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/go/zts/token/access_token.go b/clients/go/zts/token/access_token.go
new file mode 100644
index 000000000..66e4b3c08
--- /dev/null
+++ b/clients/go/zts/token/access_token.go
@@ -0,0 +1,67 @@
+package token
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+)
+
+type AccessTokenClient struct {
+	*BaseTokenClient[*AccessToken]
+}
+
+func NewAccessTokenClient(ctx context.Context, ztsUrl, pem, key string) (*AccessTokenClient, context.CancelFunc) {
+	return NewAccessTokenClientSetCacheUpdateDuration(ctx, ztsUrl, pem, key, 10*time.Minute)
+}
+
+func NewAccessTokenClientSetCacheUpdateDuration(ctx context.Context, ztsUrl, pem, key string, cacheRefreshDuration time.Duration) (*AccessTokenClient, context.CancelFunc) {
+	baseCli := newClientFunc(ztsUrl, pem, key)
+	accGetter := func(domain string, roles string, exp int32) (*AccessToken, error) {
+		req := zts.AccessTokenRequest(makeTokenRequest(domain, strings.Split(roles, ","), int(exp), ""))
+		res, err := baseCli.ZTS.PostAccessTokenRequest(req)
+		if err != nil {
+			return nil, err
+		}
+		expiryTime := time.Now().Unix() + int64(*res.Expires_in)
+		dur := time.Until(time.Unix(expiryTime, 0))
+		return &AccessToken{
+			Token:      res,
+			ExpiryTime: expiryTime,
+			Duration:   dur,
+		}, nil
+	}
+	btc := &BaseTokenClient[*AccessToken]{
+		Client:       baseCli,
+		getTokenFunc: accGetter,
+	}
+	accClient := &AccessTokenClient{BaseTokenClient: btc}
+	baseCli.Tok = accClient
+	cancel := baseCli.startBackgroundTasks(ctx, pem, key, cacheRefreshDuration)
+	return accClient, cancel
+}
+
+func makeTokenRequest(domain string, roles []string, expiryTime int, proxyPrincipalSpiffeUris string) string {
+	params := url.Values{}
+	params.Add("grant_type", "client_credentials")
+	params.Add("expires_in", fmt.Sprintf("%d", expiryTime))
+	if proxyPrincipalSpiffeUris != "" {
+		params.Add("proxy_principal_spiffe_uris", proxyPrincipalSpiffeUris)
+	}
+	var scope string
+	if roles[0] == "*" || roles[0] == "" {
+		scope = domain + ":domain"
+	} else {
+		for idx, role := range roles {
+			if idx != 0 {
+				scope += " "
+			}
+			scope += domain + ":role." + role
+		}
+	}
+	params.Add("scope", scope)
+	return params.Encode()
+}
diff --git a/clients/go/zts/token/client.go b/clients/go/zts/token/client.go
new file mode 100644
index 000000000..390dd9e2b
--- /dev/null
+++ b/clients/go/zts/token/client.go
@@ -0,0 +1,222 @@
+package token
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"net/url"
+	"os"
+	"os/signal"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+
+	"github.com/fsnotify/fsnotify"
+	"github.com/patrickmn/go-cache"
+)
+
+var newClientFunc = NewClient
+
+type GetToken interface {
+	getToken(domain string, roles string, exp int32) (interface{}, error)
+}
+
+type Client struct {
+	ZTS   ZTSClientInterface
+	Cache *cache.Cache
+	Tok   GetToken
+}
+
+func NewClient(url, pem, key string) *Client {
+	c := cache.New(5*time.Minute, 10*time.Minute)
+	return &Client{
+		ZTS:   newZTSClient(url, pem, key),
+		Cache: c,
+	}
+}
+
+func getCacheKey(domain string, roles []string, exp int32) string {
+	sort.Strings(roles)
+	r := strings.Join(roles, ",")
+	return fmt.Sprintf("%s:%s:%d", domain, r, exp)
+}
+
+func (c *Client) startBackgroundTasks(ctx context.Context, pem, key string, dur time.Duration) context.CancelFunc {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	ctx, cancel := context.WithCancel(ctx)
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+	go func() {
+		defer wg.Done()
+		if err := c.WatchCertificateFiles(ctx, pem, key); err != nil {
+			slog.Error(fmt.Sprintf("Error watching certificate files: %v", err))
+		}
+	}()
+	go func() {
+		defer wg.Done()
+		c.UpdateCachePeriodically(ctx, c.Tok, dur)
+	}()
+
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		sig := <-sigChan
+		slog.Info(fmt.Sprintf("Received signal: %v, initiating shutdown...", sig))
+		cancel()
+		wg.Wait()
+		slog.Info("All background tasks have been gracefully stopped.")
+	}()
+
+	return cancel
+}
+
+func (c *Client) WatchCertificateFiles(ctx context.Context, certFile, keyFile string) error {
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		return fmt.Errorf("failed to create file watcher: %v", err)
+	}
+	defer watcher.Close()
+	files := []string{certFile, keyFile}
+	for _, file := range files {
+		err = watcher.Add(file)
+		if err != nil {
+			return fmt.Errorf("failed to watch file %s: %v", file, err)
+		}
+	}
+	for {
+		select {
+		case <-ctx.Done():
+			slog.Info("Stopping certificate monitor...")
+			return nil
+		case event, ok := <-watcher.Events:
+			if !ok {
+				continue
+			}
+			slog.Info(fmt.Sprintf("Modified file: %s", event.Name))
+			tlsConfig, err := createTLSConfig(certFile, keyFile)
+			if err != nil {
+				slog.Error(fmt.Sprintf("Error loading certificate: %v", err))
+				continue
+			}
+			c.SetTLSConfig(tlsConfig)
+		case err, ok := <-watcher.Errors:
+			if !ok {
+				continue
+			}
+			slog.Error(fmt.Sprintf("Watcher error: %v", err))
+		}
+	}
+}
+
+func (c *Client) UpdateCachePeriodically(ctx context.Context, tk GetToken, interval time.Duration) {
+	ticker := time.NewTicker(interval)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			slog.Info("Stopping Token updater...")
+			return
+		case <-ticker.C:
+			items := c.Cache.Items()
+			for k, item := range items {
+				sp := strings.Split(k, ":")
+				exp, _ := strconv.Atoi(sp[2])
+				t, err := tk.getToken(sp[0], sp[1], int32(exp))
+				if err != nil {
+					slog.Error(fmt.Sprintf("Error fetching new token: %v", err))
+					continue
+				}
+				c.Cache.Set(k, t, item.Object.(Token).GetDuration())
+			}
+		}
+	}
+}
+
+func (c *Client) SetTLSConfig(tlsConfig *tls.Config) {
+	tp := c.ZTS.Transport()
+	if tp == nil {
+		tp = &http.Transport{}
+	}
+	newTp := tp.Clone()
+	newTp.TLSClientConfig = tlsConfig
+	c.ZTS.SetTransport(newTp)
+}
+
+func (c *Client) SetProxy(proxyURL string) {
+	p, err := url.Parse(proxyURL)
+	if err != nil {
+		slog.Error("ProxyURL is invalid")
+		return
+	}
+	tp := c.ZTS.Transport()
+	if tp == nil {
+		tp = &http.Transport{}
+	}
+	newTp := tp.Clone()
+	newTp.Proxy = http.ProxyURL(p)
+	c.ZTS.SetTransport(newTp)
+}
+
+func (c *Client) Transport() *http.Transport {
+	t := c.ZTS.Transport()
+	return t
+}
+
+func createTLSConfig(certPath, keyPath string) (*tls.Config, error) {
+	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load client certificate and key: %v", err)
+	}
+	tlsConfig := &tls.Config{
+		Certificates: []tls.Certificate{cert},
+	}
+	return tlsConfig, nil
+}
+
+type TokenGetterFunc[T Token] func(domain string, roles string, exp int32) (T, error)
+
+type BaseTokenClient[T Token] struct {
+	*Client
+	getTokenFunc TokenGetterFunc[T]
+}
+
+func (btc *BaseTokenClient[T]) getToken(domain string, roles string, exp int32) (interface{}, error) {
+	return btc.getTokenFunc(domain, roles, exp)
+}
+
+func (btc *BaseTokenClient[T]) GetTokenWithExpire(domain string, roles []string, exp int32) (T, error) {
+	currentTime := time.Now().Unix()
+	ck := getCacheKey(domain, roles, exp)
+	r := strings.Join(roles, ",")
+	if cached, found := btc.Cache.Get(ck); found && cached != nil {
+		tokenCached := cached.(T)
+		if (tokenCached.GetExpiryTime() - currentTime) > int64(tokenCached.GetDuration().Seconds())/2 {
+			return tokenCached, nil
+		}
+	}
+	tkn, err := btc.getTokenFunc(domain, r, exp)
+	if err != nil {
+		slog.Error(fmt.Sprintf("GetToken fail: %v", err))
+		if cached, found := btc.Cache.Get(ck); found && cached != nil {
+			slog.Info("Using cached token", "expire", cached.(T).GetExpiryTime())
+			return cached.(T), err
+		}
+		var zero T
+		return zero, err
+	}
+	ttl := time.Until(time.Unix(tkn.GetExpiryTime(), 0))
+	btc.Cache.Set(ck, tkn, ttl)
+	return tkn, nil
+}
+
+func (btc *BaseTokenClient[T]) GetToken(domain string, roles []string) (T, error) {
+	return btc.GetTokenWithExpire(domain, roles, 0)
+}
diff --git a/clients/go/zts/token/role_token.go b/clients/go/zts/token/role_token.go
new file mode 100644
index 000000000..a2699daaf
--- /dev/null
+++ b/clients/go/zts/token/role_token.go
@@ -0,0 +1,41 @@
+package token
+
+import (
+	"context"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+)
+
+type RoleTokenClient struct {
+	*BaseTokenClient[*RoleToken]
+}
+
+func NewRoleTokenClient(ctx context.Context, ztsUrl, pem, key string) (*RoleTokenClient, context.CancelFunc) {
+	return NewRoleTokenClientSetCacheUpdateDuration(ctx, ztsUrl, pem, key, 10*time.Minute)
+}
+
+func NewRoleTokenClientSetCacheUpdateDuration(ctx context.Context, ztsUrl, pem, key string, cacheRefreshDuration time.Duration) (*RoleTokenClient, context.CancelFunc) {
+	baseCli := newClientFunc(ztsUrl, pem, key)
+	rtGetter := func(domain string, roles string, exp int32) (*RoleToken, error) {
+		res, err := baseCli.ZTS.GetRoleToken(zts.DomainName(domain), zts.EntityList(roles), &exp, nil, "")
+		if err != nil {
+			return nil, err
+		}
+
+		dur := time.Until(time.Unix(res.ExpiryTime, 0))
+		return &RoleToken{
+			Token:      res,
+			ExpiryTime: res.ExpiryTime,
+			Duration:   dur,
+		}, nil
+	}
+	btc := &BaseTokenClient[*RoleToken]{
+		Client:       baseCli,
+		getTokenFunc: rtGetter,
+	}
+	rtClient := &RoleTokenClient{BaseTokenClient: btc}
+	baseCli.Tok = rtClient
+	cancel := baseCli.startBackgroundTasks(ctx, pem, key, cacheRefreshDuration)
+	return rtClient, cancel
+}
diff --git a/clients/go/zts/token/token.go b/clients/go/zts/token/token.go
new file mode 100644
index 000000000..a61812146
--- /dev/null
+++ b/clients/go/zts/token/token.go
@@ -0,0 +1,48 @@
+package token
+
+import (
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+)
+
+type Token interface {
+	GetExpiryTime() int64
+	GetDuration() time.Duration
+}
+
+type AccessToken struct {
+	Token      *zts.AccessTokenResponse
+	ExpiryTime int64
+	Duration   time.Duration
+}
+
+func (at *AccessToken) GetToken() *zts.AccessTokenResponse {
+	return at.Token
+}
+
+func (at *AccessToken) GetExpiryTime() int64 {
+	return at.ExpiryTime
+}
+
+func (at *AccessToken) GetDuration() time.Duration {
+	return at.Duration
+}
+
+type RoleToken struct {
+	Token      *zts.RoleToken
+	ExpiryTime int64
+	Duration   time.Duration
+}
+
+func (rt *RoleToken) GetToken() *zts.RoleToken {
+	return rt.Token
+}
+
+func (rt *RoleToken) GetExpiryTime() int64 {
+	return rt.ExpiryTime
+}
+
+func (rt *RoleToken) GetDuration() time.Duration {
+	return rt.Duration
+}
diff --git a/clients/go/zts/token/zts_client_wrapper.go b/clients/go/zts/token/zts_client_wrapper.go
new file mode 100644
index 000000000..932ab7c00
--- /dev/null
+++ b/clients/go/zts/token/zts_client_wrapper.go
@@ -0,0 +1,58 @@
+package token
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+)
+
+type ZTSClientInterface interface {
+	PostAccessTokenRequest(req zts.AccessTokenRequest) (*zts.AccessTokenResponse, error)
+	GetRoleToken(domain zts.DomainName, roles zts.EntityList, minExpiryTime *int32, maxExpiryTime *int32, proxyForPrincipal zts.EntityName) (*zts.RoleToken, error)
+	SetTransport(transport *http.Transport)
+	Transport() *http.Transport
+	URL() string
+	SetURL(url string)
+}
+
+type ztsClientWrapper struct {
+	realClient zts.ZTSClient
+}
+
+func newZTSClient(url, pem, key string) ZTSClientInterface {
+	tc, _ := createTLSConfig(pem, key)
+	tp := &http.Transport{
+		IdleConnTimeout: 30 * time.Second,
+		TLSClientConfig: tc,
+	}
+	real := zts.NewClient(url, tp)
+	return &ztsClientWrapper{realClient: real}
+}
+
+func (z *ztsClientWrapper) PostAccessTokenRequest(req zts.AccessTokenRequest) (*zts.AccessTokenResponse, error) {
+	return z.realClient.PostAccessTokenRequest(req)
+}
+
+func (z *ztsClientWrapper) GetRoleToken(domain zts.DomainName, roles zts.EntityList, minExpiryTime *int32, maxExpiryTime *int32, proxyForPrincipal zts.EntityName) (*zts.RoleToken, error) {
+	return z.realClient.GetRoleToken(domain, roles, minExpiryTime, maxExpiryTime, proxyForPrincipal)
+}
+
+func (z *ztsClientWrapper) SetTransport(t *http.Transport) {
+	z.realClient.Transport = t
+}
+
+func (z *ztsClientWrapper) Transport() *http.Transport {
+	if t, ok := z.realClient.Transport.(*http.Transport); ok {
+		return t
+	}
+	return nil
+}
+
+func (z *ztsClientWrapper) URL() string {
+	return z.realClient.URL
+}
+
+func (z *ztsClientWrapper) SetURL(url string) {
+	z.realClient.URL = url
+}
diff --git a/clients/go/zts/zts_schema.go b/clients/go/zts/zts_schema.go
index 73b891e6c..f88e9a997 100644
--- a/clients/go/zts/zts_schema.go
+++ b/clients/go/zts/zts_schema.go
@@ -340,6 +340,7 @@ func init() {
 	tOAuthConfig := rdl.NewStructTypeBuilder("Struct", "OAuthConfig")
 	tOAuthConfig.Field("issuer", "String", false, nil, "url using the https scheme")
 	tOAuthConfig.Field("authorization_endpoint", "String", false, nil, "oauth 2.0 authorization endpoint url")
+	tOAuthConfig.Field("introspection_endpoint", "String", false, nil, "oauth 2.0 introspection endpoint")
 	tOAuthConfig.Field("token_endpoint", "String", false, nil, "authorization server token endpoint")
 	tOAuthConfig.Field("jwks_uri", "String", false, nil, "public server jwk set url")
 	tOAuthConfig.ArrayField("response_types_supported", "String", false, "list of supported response types")
@@ -363,6 +364,26 @@ func init() {
 	tOIDCResponse.Field("expiration_time", "Int64", false, nil, "expiration time in UTC")
 	sb.AddType(tOIDCResponse.Build())
 
+	tIntrospectRequest := rdl.NewAliasTypeBuilder("String", "IntrospectRequest")
+	sb.AddType(tIntrospectRequest.Build())
+
+	tIntrospectResponse := rdl.NewStructTypeBuilder("Struct", "IntrospectResponse")
+	tIntrospectResponse.Field("active", "Bool", false, nil, "token status")
+	tIntrospectResponse.Field("ver", "Int32", false, nil, "version number")
+	tIntrospectResponse.Field("exp", "Int64", true, nil, "expiration time")
+	tIntrospectResponse.Field("iat", "Int64", true, nil, "issued at time")
+	tIntrospectResponse.Field("auth_time", "Int64", true, nil, "issued at time")
+	tIntrospectResponse.Field("aud", "String", true, nil, "audience")
+	tIntrospectResponse.Field("iss", "String", true, nil, "issuer")
+	tIntrospectResponse.Field("jti", "String", true, nil, "jwt id")
+	tIntrospectResponse.Field("sub", "String", true, nil, "subject")
+	tIntrospectResponse.Field("scope", "String", true, nil, "scope of the access token")
+	tIntrospectResponse.Field("client_id", "String", true, nil, "client id")
+	tIntrospectResponse.Field("uid", "String", true, nil, "user id")
+	tIntrospectResponse.Field("proxy", "String", true, nil, "proxy principal")
+	tIntrospectResponse.Field("authorization_details", "String", true, nil, "authorization details")
+	sb.AddType(tIntrospectResponse.Build())
+
 	tInstanceRegisterInformation := rdl.NewStructTypeBuilder("Struct", "InstanceRegisterInformation")
 	tInstanceRegisterInformation.Field("provider", "ServiceName", false, nil, "the provider service name (i.e. \"aws.us-west-2\", \"sys.openstack.cluster1\")")
 	tInstanceRegisterInformation.Field("domain", "DomainName", false, nil, "the domain of the instance")
@@ -1082,6 +1103,17 @@ func init() {
 	mGetOIDCResponse.Exception("UNAUTHORIZED", "ResourceError", "")
 	sb.AddResource(mGetOIDCResponse.Build())
 
+	mPostIntrospectRequest := rdl.NewResourceBuilder("IntrospectResponse", "POST", "/oauth2/introspect")
+	mPostIntrospectRequest.Comment("OAuth2 Access Token Introspection. The server will carry out the required authorization check of authorize (\"introspect\", \"{domain}:token\"); where domain is the audience of the token.")
+	mPostIntrospectRequest.Input("request", "IntrospectRequest", false, "", "", false, nil, "token introspect request details")
+	mPostIntrospectRequest.Auth("", "", true, "")
+	mPostIntrospectRequest.Exception("BAD_REQUEST", "ResourceError", "")
+	mPostIntrospectRequest.Exception("FORBIDDEN", "ResourceError", "")
+	mPostIntrospectRequest.Exception("NOT_FOUND", "ResourceError", "")
+	mPostIntrospectRequest.Exception("TOO_MANY_REQUESTS", "ResourceError", "")
+	mPostIntrospectRequest.Exception("UNAUTHORIZED", "ResourceError", "")
+	sb.AddResource(mPostIntrospectRequest.Build())
+
 	mPostRoleCertificateRequestExt := rdl.NewResourceBuilder("RoleCertificate", "POST", "/rolecert")
 	mPostRoleCertificateRequestExt.Comment("Return a TLS certificate for a role that the principal can assume. The role arn is in the CN field of the Subject and the principal is in the SAN URI field.")
 	mPostRoleCertificateRequestExt.Name("PostRoleCertificateRequestExt")
diff --git a/clients/java/msd/pom.xml b/clients/java/msd/pom.xml
index ef30e8061..56046cbf1 100644
--- a/clients/java/msd/pom.xml
+++ b/clients/java/msd/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/java/zms/pom.xml b/clients/java/zms/pom.xml
index 9b121c344..579e0566d 100644
--- a/clients/java/zms/pom.xml
+++ b/clients/java/zms/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/java/zpe/pom.xml b/clients/java/zpe/pom.xml
index 2fd27ddf5..3dd7639a5 100644
--- a/clients/java/zpe/pom.xml
+++ b/clients/java/zpe/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/java/zts/pom.xml b/clients/java/zts/pom.xml
index 6ed4f5f22..9216d6050 100644
--- a/clients/java/zts/pom.xml
+++ b/clients/java/zts/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
@@ -27,7 +27,7 @@
   <description>ZTS Java Client Library</description>
 
   <properties>
-    <code.coverage.min>0.7752</code.coverage.min>
+    <code.coverage.min>0.8035</code.coverage.min>
   </properties>
 
   <dependencyManagement>
diff --git a/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSClient.java b/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSClient.java
index ecf6890d4..87581b57a 100644
--- a/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSClient.java
+++ b/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSClient.java
@@ -19,7 +19,6 @@ package com.yahoo.athenz.zts;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InterruptedIOException;
-import java.io.UnsupportedEncodingException;
 import java.net.*;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -1232,7 +1231,7 @@ public class ZTSClient implements Closeable {
      * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
      */
     public AccessTokenResponse getAccessToken(String domainName, List<String> roleNames, long expiryTime) {
-        return getAccessToken(domainName, roleNames, null, null, null, null, expiryTime, false);
+        return getAccessToken(domainName, roleNames, null, null, null, null, null, null, expiryTime, false);
     }
 
     /**
@@ -1251,7 +1250,7 @@ public class ZTSClient implements Closeable {
      */
     public AccessTokenResponse getAccessToken(String domainName, String roleName, String authorizationDetails, long expiryTime) {
         return getAccessToken(domainName, Collections.singletonList(roleName), null, null,
-                authorizationDetails, null, expiryTime, false);
+                authorizationDetails, null, null, null, expiryTime, false);
     }
 
     /**
@@ -1270,7 +1269,7 @@ public class ZTSClient implements Closeable {
      */
     public AccessTokenResponse getAccessToken(String domainName, List<String> roleNames,
             String idTokenServiceName, long expiryTime, boolean ignoreCache) {
-        return getAccessToken(domainName, roleNames, idTokenServiceName, null, null, null, expiryTime, ignoreCache);
+        return getAccessToken(domainName, roleNames, idTokenServiceName, null, null, null, null, null, expiryTime, ignoreCache);
     }
 
     /**
@@ -1292,7 +1291,7 @@ public class ZTSClient implements Closeable {
     public AccessTokenResponse getAccessToken(String domainName, List<String> roleNames, String idTokenServiceName,
             String proxyForPrincipal, String authorizationDetails, long expiryTime, boolean ignoreCache) {
         return getAccessToken(domainName, roleNames, idTokenServiceName, proxyForPrincipal, authorizationDetails,
-                null, expiryTime, ignoreCache);
+                null, null, null, expiryTime, ignoreCache);
     }
 
     /**
@@ -1316,6 +1315,33 @@ public class ZTSClient implements Closeable {
     public AccessTokenResponse getAccessToken(String domainName, List<String> roleNames, String idTokenServiceName,
             String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris, long expiryTime,
             boolean ignoreCache) {
+        return getAccessToken(domainName, roleNames, idTokenServiceName, proxyForPrincipal, authorizationDetails,
+                proxyPrincipalSpiffeUris, null, null, expiryTime, ignoreCache);
+    }
+
+    /**
+     * For the specified requester(user/service) return the corresponding Access Token that
+     * includes the list of roles that the principal has access to in the specified domain
+     * @param domainName name of the domain
+     * @param roleNames (optional) only interested in roles with these names, comma separated list of roles
+     * @param idTokenServiceName (optional) as part of the response return an id token whose audience
+     *          is the specified service (only service name e.g. api) in the
+     *          domainName domain.
+     * @param proxyForPrincipal (optional) this request is proxy for this principal
+     * @param authorizationDetails (optional) rich authorization request details
+     * @param proxyPrincipalSpiffeUris (optional) comma separated list of spiffe uris of proxy
+     *          principals that this token will be routed through
+     * @param clientAssertionType client assertion type if not using x.509 certificates
+     * @param clientAssertion client assertion (jwt) if not using x.509 certificates
+     * @param expiryTime (optional) specifies that the returned Access must be
+     *          at least valid for specified number of seconds. Pass 0 to use
+     *          server default timeout.
+     * @param ignoreCache ignore the cache and retrieve the token from ZTS Server
+     * @return ZTS generated Access Token Response object. ZTSClientException will be thrown in case of failure
+     */
+    public AccessTokenResponse getAccessToken(String domainName, List<String> roleNames, String idTokenServiceName,
+            String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris,
+            String clientAssertionType, String clientAssertion, long expiryTime, boolean ignoreCache) {
 
         AccessTokenResponse accessTokenResponse = null;
 
@@ -1361,7 +1387,7 @@ public class ZTSClient implements Closeable {
             try {
                 final String requestBody = generateAccessTokenRequestBody(domainName, roleNames,
                         idTokenServiceName, proxyForPrincipal, authorizationDetails,
-                        proxyPrincipalSpiffeUris, expiryTime);
+                        proxyPrincipalSpiffeUris, clientAssertionType, clientAssertion, expiryTime);
                 accessTokenResponse = ztsClient.postAccessTokenRequest(requestBody);
             } catch (ClientResourceException ex) {
                 if (cacheKey != null && !ignoreCache) {
@@ -1401,7 +1427,7 @@ public class ZTSClient implements Closeable {
 
     String generateAccessTokenRequestBody(String domainName, List<String> roleNames, String idTokenServiceName,
             String proxyForPrincipal, String authorizationDetails, String proxyPrincipalSpiffeUris,
-            long expiryTime) throws UnsupportedEncodingException {
+            String clientAssertionType, String clientAssertion, long expiryTime) {
 
         StringBuilder body = new StringBuilder(256);
         body.append("grant_type=client_credentials");
@@ -1438,9 +1464,35 @@ public class ZTSClient implements Closeable {
             body.append("&proxy_principal_spiffe_uris=").append(URLEncoder.encode(proxyPrincipalSpiffeUris, StandardCharsets.UTF_8));
         }
 
+        if (!isEmpty(clientAssertionType)) {
+            body.append("&client_assertion_type=").append(URLEncoder.encode(clientAssertionType, StandardCharsets.UTF_8));
+        }
+
+        if (!isEmpty(clientAssertion)) {
+            body.append("&client_assertion=").append(URLEncoder.encode(clientAssertion, StandardCharsets.UTF_8));
+        }
+
         return body.toString();
     }
 
+    /**
+     * Introspect the given access token and return the corresponding Token Response object
+     * @param token access token to be introspected
+     * @return ZTS generated Introspect Token Response object. ZTSClientException will be thrown in case of failure
+     */
+    public IntrospectResponse introspectToken(String token) {
+
+        updateServicePrincipal();
+        try {
+            String request = "token=" + URLEncoder.encode(token, StandardCharsets.UTF_8);
+            return ztsClient.postIntrospectRequest(request);
+        } catch (ClientResourceException ex) {
+            throw new ZTSClientException(ex.getCode(), ex.getMessage());
+        } catch (Exception ex) {
+            throw new ZTSClientException(ClientResourceException.BAD_REQUEST, ex.getMessage());
+        }
+    }
+
     /**
      * For the specified requester(user/service) return the corresponding Role Certificate
      * @param domainName name of the domain
diff --git a/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSRDLGeneratedClient.java b/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSRDLGeneratedClient.java
index db27abafa..af79940d0 100644
--- a/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSRDLGeneratedClient.java
+++ b/clients/java/zts/src/main/java/com/yahoo/athenz/zts/ZTSRDLGeneratedClient.java
@@ -1039,6 +1039,35 @@ public class ZTSRDLGeneratedClient {
         }
     }
 
+    public IntrospectResponse postIntrospectRequest(String request) throws URISyntaxException, IOException {
+        UriTemplateBuilder uriTemplateBuilder = new UriTemplateBuilder(baseUrl, "/oauth2/introspect");
+        URIBuilder uriBuilder = new URIBuilder(uriTemplateBuilder.getUri());
+        HttpEntity httpEntity = new StringEntity(request, ContentType.create("application/x-www-form-urlencoded"));
+        ClassicHttpRequest httpUriRequest = ClassicRequestBuilder.post()
+            .setUri(uriBuilder.build())
+            .setEntity(httpEntity)
+            .build();
+        if (credsHeader != null) {
+            httpUriRequest.addHeader(credsHeader, credsToken);
+        }
+        HttpEntity httpResponseEntity = null;
+        try (CloseableHttpResponse httpResponse = client.execute(httpUriRequest, httpContext)) {
+            int code = httpResponse.getCode();
+            httpResponseEntity = httpResponse.getEntity();
+            switch (code) {
+            case 200:
+                return jsonMapper.readValue(httpResponseEntity.getContent(), IntrospectResponse.class);
+            default:
+                final String errorData = (httpResponseEntity == null) ? null : getStringResponseEntity(httpResponseEntity);
+                throw (errorData != null && !errorData.isEmpty())
+                    ? new ClientResourceException(code, jsonMapper.readValue(errorData, ClientResourceError.class))
+                    : new ClientResourceException(code);
+            }
+        } finally {
+            EntityUtils.consumeQuietly(httpResponseEntity);
+        }
+    }
+
     public RoleCertificate postRoleCertificateRequestExt(RoleCertificateRequest req) throws URISyntaxException, IOException {
         UriTemplateBuilder uriTemplateBuilder = new UriTemplateBuilder(baseUrl, "/rolecert");
         URIBuilder uriBuilder = new URIBuilder(uriTemplateBuilder.getUri());
diff --git a/clients/nodejs/zpe/pom.xml b/clients/nodejs/zpe/pom.xml
index c427d7796..c984c8aaf 100644
--- a/clients/nodejs/zpe/pom.xml
+++ b/clients/nodejs/zpe/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/clients/nodejs/zts/pom.xml b/clients/nodejs/zts/pom.xml
index 77ffe177d..0ffd4bc14 100644
--- a/clients/nodejs/zts/pom.xml
+++ b/clients/nodejs/zts/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/containers/jetty/pom.xml b/containers/jetty/pom.xml
index 2d00fb5d6..0670554ee 100644
--- a/containers/jetty/pom.xml
+++ b/containers/jetty/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/core/msd/pom.xml b/core/msd/pom.xml
index dc44d220a..c24b05d55 100644
--- a/core/msd/pom.xml
+++ b/core/msd/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/core/zms/pom.xml b/core/zms/pom.xml
index b94fcf0a2..d69c5bc9c 100644
--- a/core/zms/pom.xml
+++ b/core/zms/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/core/zts/pom.xml b/core/zts/pom.xml
index 28b2b51d7..fa58eeac8 100644
--- a/core/zts/pom.xml
+++ b/core/zts/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/core/zts/src/main/java/com/yahoo/athenz/zts/IntrospectResponse.java b/core/zts/src/main/java/com/yahoo/athenz/zts/IntrospectResponse.java
new file mode 100644
index 000000000..68c52d024
--- /dev/null
+++ b/core/zts/src/main/java/com/yahoo/athenz/zts/IntrospectResponse.java
@@ -0,0 +1,205 @@
+//
+// This file generated by rdl 1.5.2. Do not modify!
+//
+
+package com.yahoo.athenz.zts;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.yahoo.rdl.*;
+
+//
+// IntrospectResponse -
+//
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class IntrospectResponse {
+    public boolean active;
+    public int ver;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public Long exp;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public Long iat;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public Long auth_time;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String aud;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String iss;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String jti;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String sub;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String scope;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String client_id;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String uid;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String proxy;
+    @RdlOptional
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    public String authorization_details;
+
+    public IntrospectResponse setActive(boolean active) {
+        this.active = active;
+        return this;
+    }
+    public boolean getActive() {
+        return active;
+    }
+    public IntrospectResponse setVer(int ver) {
+        this.ver = ver;
+        return this;
+    }
+    public int getVer() {
+        return ver;
+    }
+    public IntrospectResponse setExp(Long exp) {
+        this.exp = exp;
+        return this;
+    }
+    public Long getExp() {
+        return exp;
+    }
+    public IntrospectResponse setIat(Long iat) {
+        this.iat = iat;
+        return this;
+    }
+    public Long getIat() {
+        return iat;
+    }
+    public IntrospectResponse setAuth_time(Long auth_time) {
+        this.auth_time = auth_time;
+        return this;
+    }
+    public Long getAuth_time() {
+        return auth_time;
+    }
+    public IntrospectResponse setAud(String aud) {
+        this.aud = aud;
+        return this;
+    }
+    public String getAud() {
+        return aud;
+    }
+    public IntrospectResponse setIss(String iss) {
+        this.iss = iss;
+        return this;
+    }
+    public String getIss() {
+        return iss;
+    }
+    public IntrospectResponse setJti(String jti) {
+        this.jti = jti;
+        return this;
+    }
+    public String getJti() {
+        return jti;
+    }
+    public IntrospectResponse setSub(String sub) {
+        this.sub = sub;
+        return this;
+    }
+    public String getSub() {
+        return sub;
+    }
+    public IntrospectResponse setScope(String scope) {
+        this.scope = scope;
+        return this;
+    }
+    public String getScope() {
+        return scope;
+    }
+    public IntrospectResponse setClient_id(String client_id) {
+        this.client_id = client_id;
+        return this;
+    }
+    public String getClient_id() {
+        return client_id;
+    }
+    public IntrospectResponse setUid(String uid) {
+        this.uid = uid;
+        return this;
+    }
+    public String getUid() {
+        return uid;
+    }
+    public IntrospectResponse setProxy(String proxy) {
+        this.proxy = proxy;
+        return this;
+    }
+    public String getProxy() {
+        return proxy;
+    }
+    public IntrospectResponse setAuthorization_details(String authorization_details) {
+        this.authorization_details = authorization_details;
+        return this;
+    }
+    public String getAuthorization_details() {
+        return authorization_details;
+    }
+
+    @Override
+    public boolean equals(Object another) {
+        if (this != another) {
+            if (another == null || another.getClass() != IntrospectResponse.class) {
+                return false;
+            }
+            IntrospectResponse a = (IntrospectResponse) another;
+            if (active != a.active) {
+                return false;
+            }
+            if (ver != a.ver) {
+                return false;
+            }
+            if (exp == null ? a.exp != null : !exp.equals(a.exp)) {
+                return false;
+            }
+            if (iat == null ? a.iat != null : !iat.equals(a.iat)) {
+                return false;
+            }
+            if (auth_time == null ? a.auth_time != null : !auth_time.equals(a.auth_time)) {
+                return false;
+            }
+            if (aud == null ? a.aud != null : !aud.equals(a.aud)) {
+                return false;
+            }
+            if (iss == null ? a.iss != null : !iss.equals(a.iss)) {
+                return false;
+            }
+            if (jti == null ? a.jti != null : !jti.equals(a.jti)) {
+                return false;
+            }
+            if (sub == null ? a.sub != null : !sub.equals(a.sub)) {
+                return false;
+            }
+            if (scope == null ? a.scope != null : !scope.equals(a.scope)) {
+                return false;
+            }
+            if (client_id == null ? a.client_id != null : !client_id.equals(a.client_id)) {
+                return false;
+            }
+            if (uid == null ? a.uid != null : !uid.equals(a.uid)) {
+                return false;
+            }
+            if (proxy == null ? a.proxy != null : !proxy.equals(a.proxy)) {
+                return false;
+            }
+            if (authorization_details == null ? a.authorization_details != null : !authorization_details.equals(a.authorization_details)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/core/zts/src/main/java/com/yahoo/athenz/zts/OAuthConfig.java b/core/zts/src/main/java/com/yahoo/athenz/zts/OAuthConfig.java
index 9eb919a59..c6b4d9804 100644
--- a/core/zts/src/main/java/com/yahoo/athenz/zts/OAuthConfig.java
+++ b/core/zts/src/main/java/com/yahoo/athenz/zts/OAuthConfig.java
@@ -14,6 +14,7 @@ import com.yahoo.rdl.*;
 public class OAuthConfig {
     public String issuer;
     public String authorization_endpoint;
+    public String introspection_endpoint;
     public String token_endpoint;
     public String jwks_uri;
     public List<String> response_types_supported;
@@ -34,6 +35,13 @@ public class OAuthConfig {
     public String getAuthorization_endpoint() {
         return authorization_endpoint;
     }
+    public OAuthConfig setIntrospection_endpoint(String introspection_endpoint) {
+        this.introspection_endpoint = introspection_endpoint;
+        return this;
+    }
+    public String getIntrospection_endpoint() {
+        return introspection_endpoint;
+    }
     public OAuthConfig setToken_endpoint(String token_endpoint) {
         this.token_endpoint = token_endpoint;
         return this;
@@ -83,6 +91,9 @@ public class OAuthConfig {
             if (authorization_endpoint == null ? a.authorization_endpoint != null : !authorization_endpoint.equals(a.authorization_endpoint)) {
                 return false;
             }
+            if (introspection_endpoint == null ? a.introspection_endpoint != null : !introspection_endpoint.equals(a.introspection_endpoint)) {
+                return false;
+            }
             if (token_endpoint == null ? a.token_endpoint != null : !token_endpoint.equals(a.token_endpoint)) {
                 return false;
             }
diff --git a/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java b/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java
index a6571b96c..93b880c87 100644
--- a/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java
+++ b/core/zts/src/main/java/com/yahoo/athenz/zts/ZTSSchema.java
@@ -292,6 +292,7 @@ public class ZTSSchema {
         sb.structType("OAuthConfig")
             .field("issuer", "String", false, "url using the https scheme")
             .field("authorization_endpoint", "String", false, "oauth 2.0 authorization endpoint url")
+            .field("introspection_endpoint", "String", false, "oauth 2.0 introspection endpoint")
             .field("token_endpoint", "String", false, "authorization server token endpoint")
             .field("jwks_uri", "String", false, "public server jwk set url")
             .arrayField("response_types_supported", "String", false, "list of supported response types")
@@ -311,6 +312,24 @@ public class ZTSSchema {
             .field("success", "Bool", false, "response status")
             .field("expiration_time", "Int64", false, "expiration time in UTC");
 
+    
+
+        sb.structType("IntrospectResponse")
+            .field("active", "Bool", false, "token status")
+            .field("ver", "Int32", false, "version number")
+            .field("exp", "Int64", true, "expiration time")
+            .field("iat", "Int64", true, "issued at time")
+            .field("auth_time", "Int64", true, "issued at time")
+            .field("aud", "String", true, "audience")
+            .field("iss", "String", true, "issuer")
+            .field("jti", "String", true, "jwt id")
+            .field("sub", "String", true, "subject")
+            .field("scope", "String", true, "scope of the access token")
+            .field("client_id", "String", true, "client id")
+            .field("uid", "String", true, "user id")
+            .field("proxy", "String", true, "proxy principal")
+            .field("authorization_details", "String", true, "authorization details");
+
         sb.structType("InstanceRegisterInformation")
             .field("provider", "ServiceName", false, "the provider service name (i.e. \"aws.us-west-2\", \"sys.openstack.cluster1\")")
             .field("domain", "DomainName", false, "the domain of the instance")
@@ -1101,6 +1120,22 @@ public class ZTSSchema {
             .exception("UNAUTHORIZED", "ResourceError", "")
 ;
 
+        sb.resource("IntrospectRequest", "POST", "/oauth2/introspect")
+            .comment("OAuth2 Access Token Introspection. The server will carry out the required authorization check of authorize (\"introspect\", \"{domain}:token\"); where domain is the audience of the token.")
+            .input("request", "IntrospectRequest", "token introspect request details")
+            .auth("", "", true)
+            .expected("OK")
+            .exception("BAD_REQUEST", "ResourceError", "")
+
+            .exception("FORBIDDEN", "ResourceError", "")
+
+            .exception("NOT_FOUND", "ResourceError", "")
+
+            .exception("TOO_MANY_REQUESTS", "ResourceError", "")
+
+            .exception("UNAUTHORIZED", "ResourceError", "")
+;
+
         sb.resource("RoleCertificateRequest", "POST", "/rolecert")
             .comment("Return a TLS certificate for a role that the principal can assume. The role arn is in the CN field of the Subject and the principal is in the SAN URI field.")
             .name("PostRoleCertificateRequestExt")
diff --git a/core/zts/src/main/rdl/OAuth.rdli b/core/zts/src/main/rdl/OAuth.rdli
index ff3e5114f..ee881bbae 100644
--- a/core/zts/src/main/rdl/OAuth.rdli
+++ b/core/zts/src/main/rdl/OAuth.rdli
@@ -69,3 +69,19 @@ resource OIDCResponse GET "/oauth2/auth?response_type={responseType}&client_id={
         ResourceError TOO_MANY_REQUESTS;
     }
 }
+
+// OAuth2 Access Token Introspection. The server will carry out the required
+// authorization check of authorize ("introspect", "{domain}:token");
+// where domain is the audience of the token.
+resource IntrospectResponse POST "/oauth2/introspect" {
+    IntrospectRequest request; //token introspect request details
+    consumes application/x-www-form-urlencoded
+    authenticate;
+    exceptions {
+        ResourceError BAD_REQUEST;
+        ResourceError FORBIDDEN;
+        ResourceError NOT_FOUND;
+        ResourceError UNAUTHORIZED;
+        ResourceError TOO_MANY_REQUESTS;
+    }
+}
diff --git a/core/zts/src/main/rdl/OAuth.tdl b/core/zts/src/main/rdl/OAuth.tdl
index 09f0b8fbe..f50381492 100644
--- a/core/zts/src/main/rdl/OAuth.tdl
+++ b/core/zts/src/main/rdl/OAuth.tdl
@@ -38,6 +38,7 @@ type OpenIDConfig Struct {
 type OAuthConfig Struct {
     String issuer; //url using the https scheme
     String authorization_endpoint; //oauth 2.0 authorization endpoint url
+    String introspection_endpoint; //oauth 2.0 introspection endpoint
     String token_endpoint; //authorization server token endpoint
     String jwks_uri; //public server jwk set url
     Array<String> response_types_supported; //list of supported response types
@@ -59,3 +60,22 @@ type OIDCResponse Struct {
     Bool success; //response status
     Int64 expiration_time; //expiration time in UTC
 }
+
+type IntrospectRequest String;
+
+type IntrospectResponse Struct {
+    Bool active; //token status
+    Int32 ver; //version number
+    Int64 exp (optional); //expiration time
+    Int64 iat (optional); //issued at time
+    Int64 auth_time (optional); //issued at time
+    String aud (optional); //audience
+    String iss (optional); //issuer
+    String jti (optional); //jwt id
+    String sub (optional); //subject
+    String scope (optional); //scope of the access token
+    String client_id (optional); //client id
+    String uid (optional); //user id
+    String proxy (optional); //proxy principal
+    String authorization_details (optional); //authorization details
+}
diff --git a/docker/db/zms/Dockerfile b/docker/db/zms/Dockerfile
index 14d8d9ed9..fb7934852 100644
--- a/docker/db/zms/Dockerfile
+++ b/docker/db/zms/Dockerfile
@@ -25,6 +25,7 @@ ENTRYPOINT ["docker-entrypoint.sh"]
 # EXPOSE 3306
 CMD ["mysqld"]
 
+# Do not use the built-in root, sa or SYS accounts for production setup
 HEALTHCHECK --interval=1m --timeout=3s --start-period=10s --retries=3 \
   CMD mysql -sss \
   --host localhost \
diff --git a/docker/db/zts/Dockerfile b/docker/db/zts/Dockerfile
index 193b56f23..124f93009 100644
--- a/docker/db/zts/Dockerfile
+++ b/docker/db/zts/Dockerfile
@@ -25,6 +25,7 @@ ENTRYPOINT ["docker-entrypoint.sh"]
 # EXPOSE 3306
 CMD ["mysqld"]
 
+# Do not use the built-in root, sa or SYS accounts for production setup
 HEALTHCHECK --interval=1m --timeout=3s --start-period=10s --retries=3 \
   CMD mysql -sss \
   --host localhost \
diff --git a/go.mod b/go.mod
index fa2f48e62..512ec9399 100644
--- a/go.mod
+++ b/go.mod
@@ -5,17 +5,17 @@ toolchain go1.23.3
 module github.com/AthenZ/athenz
 
 require (
-	cloud.google.com/go/secretmanager v1.14.4
+	cloud.google.com/go/secretmanager v1.14.5
 	github.com/ardielle/ardielle-go v1.5.2
-	github.com/aws/aws-sdk-go-v2 v1.36.1
-	github.com/aws/aws-sdk-go-v2/config v1.29.6
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.18
-	github.com/aws/aws-sdk-go-v2/service/ssm v1.56.12
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.14
+	github.com/aws/aws-sdk-go-v2 v1.36.2
+	github.com/aws/aws-sdk-go-v2/config v1.29.7
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19
+	github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.15
 	github.com/cenkalti/backoff v2.2.1+incompatible
 	github.com/dimfeld/httptreemux v5.0.1+incompatible
 	github.com/envoyproxy/go-control-plane v0.13.1
-	github.com/go-jose/go-jose/v4 v4.0.4
+	github.com/go-jose/go-jose/v4 v4.0.5
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/golang/mock v1.6.0
 	github.com/google/go-cmp v0.6.0
@@ -26,58 +26,53 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
 	golang.org/x/crypto v0.33.0
-	golang.org/x/net v0.34.0
+	golang.org/x/net v0.35.0
 	golang.org/x/oauth2 v0.26.0
 	google.golang.org/grpc v1.70.0
 	google.golang.org/protobuf v1.36.5
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/apimachinery v0.32.1
-	k8s.io/client-go v0.32.1
+	k8s.io/apimachinery v0.32.2
+	k8s.io/client-go v0.32.2
 	k8s.io/utils v0.0.0-20241210054802-24370beab758
 )
 
 require (
-	cloud.google.com/go/auth v0.14.0 // indirect
+	cloud.google.com/go/auth v0.14.1 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
-	cloud.google.com/go/compute v1.31.1 // indirect
 	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	cloud.google.com/go/iam v1.3.1 // indirect
-	github.com/ardielle/ardielle-tools v1.5.4 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.59 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.60 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 // indirect
 	github.com/aws/smithy-go v1.22.2 // indirect
 	github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
-	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/kr/text v0.2.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
@@ -89,11 +84,10 @@ require (
 	golang.org/x/term v0.29.0 // indirect
 	golang.org/x/text v0.22.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
-	google.golang.org/api v0.218.0 // indirect
-	google.golang.org/appengine v1.6.8 // indirect
+	google.golang.org/api v0.220.0 // indirect
 	google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
diff --git a/go.sum b/go.sum
index be1ae74f0..d423f29e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,80 +1,69 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
-cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
+cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ=
+cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM=
+cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
+cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
 cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
 cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
-cloud.google.com/go/compute v1.31.1 h1:SObuy8Fs6woazArpXp1fsHCw+ZH4iJ/8dGGTxUhHZQA=
-cloud.google.com/go/compute v1.31.1/go.mod h1:hyOponWhXviDptJCJSoEh89XO1cfv616wbwbkde1/+8=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E=
 cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34=
-cloud.google.com/go/secretmanager v1.14.4 h1:SMWQMsUcACsdIuVhIBAw+QfKY4Xseiaa8qDnunjmhcM=
-cloud.google.com/go/secretmanager v1.14.4/go.mod h1:pjwFw8+A6B4AcWrVXruLfz1QykkpMr8T/VT+zXB91iw=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sCosDBters=
+cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY=
 github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4=
 github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI=
-github.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dEx4jeNG6A+AdkShk=
-github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E=
-github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
-github.com/aws/aws-sdk-go-v2/config v1.29.6 h1:fqgqEKK5HaZVWLQoLiC9Q+xDlSp+1LYidp6ybGE2OGg=
-github.com/aws/aws-sdk-go-v2/config v1.29.6/go.mod h1:Ft+WLODzDQmCTHDvqAH1JfC2xxbZ0MxpZAcJqmE1LTQ=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p7tssS9yAsGe8aKP4=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.18 h1:U/gg5eOAPx9vzip9A6cQ2GkIAPBthHMaKDfZ/WWEuj0=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.18/go.mod h1:ul2OTb6zT/dpZX/2bxKVwa6eIDBBlPNuau9uZuIoRAI=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.56.12 h1:EKEY56SQTqEsOuh68B8YVqmsLJ1nuwUGYyKImyo+0ug=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.56.12/go.mod h1:I/j1db6MPxBp7vcVrRAh+u+vERu79MWoyhoSjRaDl9E=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 h1:/eE3DogBjYlvlbhd2ssWyeuovWunHLxfgw3s/OJa4GQ=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.15/go.mod h1:2PCJYpi7EKeA5SkStAmZlF6fi0uUABuhtF8ILHjGc3Y=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 h1:M/zwXiL2iXUrHputuXgmO94TVNmcenPHxgLXLutodKE=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14/go.mod h1:RVwIw3y/IqxC2YEXSIkAzRDdEU1iRabDPaYjpGCbCGQ=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy+G8qQN/QVYzGLkQE=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc=
+github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU=
+github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M=
+github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19 h1:O2xbipq7k1kTct69V7mFidwTagld9c/6iyK+3yo+QNg=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19/go.mod h1:CxTOwBy2Qs8/+yV7fkz4eZB1RB5qeWaW9SvznvFLgRA=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13 h1:JfPeW7F6Y+VqBg6p+8zQv4wlgceguYu5ZT0USEGZ89g=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.56.13/go.mod h1:EonGQFn66wZkJJrrKXrryrxoS3V30rcHvaWvc6oGHCI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15/go.mod h1:5uPZU7vSNzb8Y0dm75xTikinegPYK3uJmIHQZFq5Aqo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/9NILXXu42FP3rJg0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8=
 github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
 github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
 github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 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=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
 github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
 github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
-github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
-github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
-github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -84,31 +73,12 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -119,7 +89,6 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
@@ -128,35 +97,36 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk
 github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk=
+github.com/jawher/mow.cli v1.2.0 h1:e6ViPPy+82A/NFF/cfbq3Lr6q4JHKT9tyHwTCcUQgQw=
 github.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
@@ -166,7 +136,7 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
@@ -178,139 +148,90 @@ go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
 go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
 go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
 go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
+go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
 go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
 golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
 golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
 golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
 golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
 golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
 golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=
-google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns=
+google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY=
 google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s=
 google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 h1:5iw9XJTD4thFidQmFVvx0wi4g5yOHk76rNRUxz1ZG5g=
-google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 h1:L9JNMl/plZH9wmzQUHleO/ZZDSN+9Gh41wPczNy+5Fk=
+google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
 google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
 google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
-k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
-k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
+k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
+k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
+k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
diff --git a/libs/go/athenzconf/pom.xml b/libs/go/athenzconf/pom.xml
index 88a61d6ac..a6ac1e61b 100644
--- a/libs/go/athenzconf/pom.xml
+++ b/libs/go/athenzconf/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/go/athenzutils/pom.xml b/libs/go/athenzutils/pom.xml
index ca6cb2de7..2597da479 100644
--- a/libs/go/athenzutils/pom.xml
+++ b/libs/go/athenzutils/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/go/athenzutils/privkey.go b/libs/go/athenzutils/privkey.go
index 079d180b6..fa1280273 100644
--- a/libs/go/athenzutils/privkey.go
+++ b/libs/go/athenzutils/privkey.go
@@ -10,7 +10,7 @@ import (
 	"reflect"
 )
 
-// ExtractSignerInfo extract crypto.Signer and x509.SignatureAlgorithm from the given private key (ECDSA or RSA).
+// ExtractSignerInfo extracts crypto.Signer and x509.SignatureAlgorithm from the given private key (ECDSA or RSA).
 func ExtractSignerInfo(privateKeyPEM []byte) (crypto.Signer, x509.SignatureAlgorithm, error) {
 	block, _ := pem.Decode(privateKeyPEM)
 	if block == nil {
diff --git a/libs/go/sia/pom.xml b/libs/go/sia/pom.xml
index 859c5bec3..969d55d95 100644
--- a/libs/go/sia/pom.xml
+++ b/libs/go/sia/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/go/sia/util/util.go b/libs/go/sia/util/util.go
index ad282e148..296e6a759 100644
--- a/libs/go/sia/util/util.go
+++ b/libs/go/sia/util/util.go
@@ -27,7 +27,6 @@ import (
 	"encoding/json"
 	"encoding/pem"
 	"fmt"
-	"github.com/google/shlex"
 	"log"
 	"net"
 	"net/http"
@@ -39,11 +38,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ardielle/ardielle-go/rdl"
-
 	"github.com/AthenZ/athenz/clients/go/zts"
 	"github.com/AthenZ/athenz/libs/go/sia/futil"
 	"github.com/AthenZ/athenz/libs/go/tls/config"
+	"github.com/ardielle/ardielle-go/rdl"
+	"github.com/google/shlex"
 )
 
 // CertReqDetails - struct with details to generate a certificate CSR
diff --git a/libs/go/zmscli/pom.xml b/libs/go/zmscli/pom.xml
index d6e5afe25..ed7c605b3 100644
--- a/libs/go/zmscli/pom.xml
+++ b/libs/go/zmscli/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/go/zmssvctoken/pom.xml b/libs/go/zmssvctoken/pom.xml
index 6e7c85593..856be023c 100644
--- a/libs/go/zmssvctoken/pom.xml
+++ b/libs/go/zmssvctoken/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/auth_core/pom.xml b/libs/java/auth_core/pom.xml
index 3f1a5735c..ef4d7828a 100644
--- a/libs/java/auth_core/pom.xml
+++ b/libs/java/auth_core/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
@@ -27,7 +27,7 @@
   <description>Core Auth Interfaces</description>
 
   <properties>
-    <code.coverage.min>0.9140</code.coverage.min>
+    <code.coverage.min>0.9170</code.coverage.min>
   </properties>
 
   <dependencies>
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityConsts.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityConsts.java
index 2402fe0db..d35a8eab8 100644
--- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityConsts.java
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityConsts.java
@@ -21,6 +21,7 @@ package com.yahoo.athenz.auth;
 public final class AuthorityConsts {
 
     // Athenz data model
+
     public static final char ATHENZ_PRINCIPAL_DELIMITER_CHAR = '.';
     public static final String ATHENZ_PRINCIPAL_DELIMITER = String.valueOf(ATHENZ_PRINCIPAL_DELIMITER_CHAR);
 
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityKeyStore.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityKeyStore.java
index 30c326e47..747b6a24a 100644
--- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityKeyStore.java
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/AuthorityKeyStore.java
@@ -17,5 +17,11 @@ package com.yahoo.athenz.auth;
 
 public interface AuthorityKeyStore {
 
+    /**
+     * Set the key store to be used by the authority. This object
+     * is expected to be used by the authority to retrieve public
+     * keys for the given service identities in pem format
+     * @param keyStore KeyStore object
+     */
     void setKeyStore(KeyStore keyStore);
 }
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/KeyStore.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/KeyStore.java
index e351adb6a..a3a04197b 100644
--- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/KeyStore.java
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/KeyStore.java
@@ -15,6 +15,8 @@
  */
 package com.yahoo.athenz.auth;
 
+import java.security.PublicKey;
+
 public interface KeyStore {
     
     /**
@@ -25,5 +27,17 @@ public interface KeyStore {
      * @param keyId the public key identifier
      * @return String with PEM encoded key
      */
+    @Deprecated
     String getPublicKey(String domain, String service, String keyId);
+
+    /**
+     * Return the public key for the given key id and service.
+     * @param domain Name of the domain
+     * @param service Name of the service
+     * @param keyId the public key identifier
+     * @return Public Key object or null if not found
+     */
+    default PublicKey getServicePublicKey(String domain, String service, String keyId) {
+        return null;
+    }
 }
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthority.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthority.java
new file mode 100644
index 000000000..5c75051c2
--- /dev/null
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthority.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yahoo.athenz.auth.impl;
+
+import com.yahoo.athenz.auth.*;
+import com.yahoo.athenz.auth.token.OAuth2Token;
+import com.yahoo.athenz.auth.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ServiceAccessTokenAuthority implements Authority, AuthorityKeyStore {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceAccessTokenAuthority.class);
+
+    public static final String ATHENZ_AUTH_CHALLENGE = "Basic realm=\"athenz\"";
+    public static final String HTTP_HEADER = "Authorization";
+    public static final String HTTP_BEARER_PREFIX = "Bearer ";
+
+    public static final String PROP_OAUTH_ISSUER = "athenz.auth.access_token.oauth_issuer";
+
+    private final String oauth2Issuer;
+    private KeyStore keyStore;
+
+    public ServiceAccessTokenAuthority() {
+        oauth2Issuer = System.getProperty(PROP_OAUTH_ISSUER, "https://athenz.io");
+    }
+
+    @Override
+    public String getID() {
+        return "Auth-SvcAccessToken";
+    }
+
+    @Override
+    public void initialize() {
+    }
+
+    @Override
+    public String getDomain() {
+        return null;
+    }
+
+    @Override
+    public String getHeader() {
+        return HTTP_HEADER;
+    }
+
+    @Override
+    public String getAuthenticateChallenge() {
+        return ATHENZ_AUTH_CHALLENGE;
+    }
+
+    @Override
+    public Principal authenticate(String creds, String remoteAddr, String httpMethod, StringBuilder errMsg) {
+
+        errMsg = errMsg == null ? new StringBuilder(512) : errMsg;
+
+        if (StringUtils.isEmpty(creds)) {
+            errMsg.append("AccessTokenAuthority:authenticate: No credentials provided");
+            LOG.error(errMsg.toString());
+            return null;
+        }
+
+        if (!creds.startsWith(HTTP_BEARER_PREFIX)) {
+            errMsg.append("AccessTokenAuthority:authenticate: Invalid token: No Bearer prefix");
+            LOG.error(errMsg.toString());
+            return null;
+        }
+
+        OAuth2Token token;
+        try {
+            token = new OAuth2Token(creds.substring(HTTP_BEARER_PREFIX.length()), keyStore, oauth2Issuer);
+        } catch (Exception ex) {
+            errMsg.append("AccessTokenAuthority:authenticate: Invalid token: exc=").append(ex.getMessage());
+            LOG.error(errMsg.toString());
+            return null;
+        }
+
+        return SimplePrincipal.create(token.getClientIdDomainName(), token.getClientIdServiceName(),
+                creds, token.getIssueTime(), this);
+    }
+
+    @Override
+    public void setKeyStore(KeyStore keyStore) {
+        this.keyStore = keyStore;
+    }
+}
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java
index be740911a..47c297f89 100644
--- a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/OAuth2Token.java
@@ -19,9 +19,11 @@ import com.nimbusds.jose.*;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jwt.*;
 import com.nimbusds.jwt.proc.*;
+import com.yahoo.athenz.auth.KeyStore;
 import com.yahoo.athenz.auth.token.jwts.JwtsHelper;
 import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver;
 import com.yahoo.athenz.auth.util.CryptoException;
+import com.yahoo.athenz.auth.util.StringUtils;
 
 import java.security.PublicKey;
 import java.util.Date;
@@ -41,6 +43,8 @@ public class OAuth2Token {
     protected String issuer;
     protected String subject;
     protected String jwtId;
+    protected String clientIdDomainName;
+    protected String clientIdServiceName;
     protected JWTClaimsSet claimsSet = null;
     protected static DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier = new DefaultJWTClaimsVerifier<>(null, null);
 
@@ -106,6 +110,77 @@ public class OAuth2Token {
         }
     }
 
+    public OAuth2Token(final String token, KeyStore publicKeyProvider, final String oauth2Issuer) {
+
+        try {
+            // first parse the token to extract the fields from the body and header
+
+            SignedJWT signedJWT = SignedJWT.parse(token);
+
+            // Extract and verify the claims (expiry, not before, etc.)
+
+            claimsSet = signedJWT.getJWTClaimsSet();
+            claimsVerifier.verify(claimsSet, null);
+
+            // extract the issuer and subject of the token. for athenz supported case
+            // these values must be present and equal
+
+            final String issuer = claimsSet.getIssuer();
+            final String subject = claimsSet.getSubject();
+            if (StringUtils.isEmpty(issuer) || StringUtils.isEmpty(subject) || !issuer.equals(subject)) {
+                throw new CryptoException("Invalid token: missing or mismatched issuer and subject");
+            }
+
+            // extract the audience of the token. for athenz support case this
+            // value must be present and match the oidc issuer for the server
+
+            final String audience = getClaimAudience();
+            if (StringUtils.isEmpty(audience) || !audience.equals(oauth2Issuer)) {
+                throw new CryptoException("Invalid token: missing or mismatched audience");
+            }
+
+            // our issuer/subject is the service identifier so we'll extract
+            // the domain and service values
+
+            int idx = subject.lastIndexOf('.');
+            if (idx < 0) {
+                throw new CryptoException("Invalid token: missing domain and service");
+            }
+
+            clientIdDomainName = subject.substring(0, idx);
+            clientIdServiceName = subject.substring(idx + 1);
+
+            // extract the key id from the header
+
+            JWSHeader header = signedJWT.getHeader();
+            String keyId = header.getKeyID();
+            if (StringUtils.isEmpty(keyId)) {
+                throw new CryptoException("Invalid token: missing key id");
+            }
+
+            // get the public key for the domain/service/key id
+            // and create a verifier
+
+            final PublicKey publicKey = publicKeyProvider.getServicePublicKey(clientIdDomainName, clientIdServiceName, keyId);
+            if (publicKey == null) {
+                throw new CryptoException("Invalid token: unable to get public key");
+            }
+
+            JWSVerifier verifier = JwtsHelper.getJWSVerifier(publicKey);
+            if (!signedJWT.verify(verifier)) {
+                throw new CryptoException("Unable to verify token signature");
+            }
+
+            // if we got this far then we have a valid token so
+            // we'll set our claims set and extract the fields
+
+            setTokenFields();
+
+        } catch (Exception ex) {
+            throw new CryptoException("Unable to parse token: " + ex.getMessage());
+        }
+    }
+
     // our date values are stored in seconds
 
     long parseDateValue(Date date) {
@@ -115,10 +190,7 @@ public class OAuth2Token {
     void setTokenFields() {
 
         setVersion(JwtsHelper.getIntegerClaim(claimsSet, CLAIM_VERSION, 0));
-        List<String> audiences = claimsSet.getAudience();
-        if (audiences != null && !audiences.isEmpty()) {
-            setAudience(audiences.get(0));
-        }
+        setAudience(getClaimAudience());
         setExpiryTime(parseDateValue(claimsSet.getExpirationTime()));
         setIssueTime(parseDateValue(claimsSet.getIssueTime()));
         setNotBeforeTime(parseDateValue(claimsSet.getNotBeforeTime()));
@@ -128,6 +200,14 @@ public class OAuth2Token {
         setJwtId(claimsSet.getJWTID());
     }
 
+    String getClaimAudience() {
+        List<String> audiences = claimsSet.getAudience();
+        if (audiences != null && !audiences.isEmpty()) {
+            return audiences.get(0);
+        }
+        return null;
+    }
+
     public int getVersion() {
         return version;
     }
@@ -199,4 +279,12 @@ public class OAuth2Token {
     public void setJwtId(String jwtId) {
         this.jwtId = jwtId;
     }
+
+    public String getClientIdDomainName() {
+        return clientIdDomainName;
+    }
+
+    public String getClientIdServiceName() {
+        return clientIdServiceName;
+    }
 }
diff --git a/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/ZTSAccessToken.java b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/ZTSAccessToken.java
new file mode 100644
index 000000000..d4375f94e
--- /dev/null
+++ b/libs/java/auth_core/src/main/java/com/yahoo/athenz/auth/token/ZTSAccessToken.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yahoo.athenz.auth.token;
+
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jwt.SignedJWT;
+import com.yahoo.athenz.auth.Authorizer;
+import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.Principal;
+import com.yahoo.athenz.auth.token.jwts.JwtsHelper;
+import com.yahoo.athenz.auth.util.CryptoException;
+import com.yahoo.athenz.auth.util.StringUtils;
+
+import java.security.PublicKey;
+
+public class ZTSAccessToken extends AccessToken {
+
+    private static final String ACTION_INTROSPECT = "introspect";
+    private static final String RESOURCE_TOKEN = ":token";
+    private static final String SYS_AUTH_DOMAIN = "sys.auth";
+    private static final String ZTS_SERVICE = "zts";
+
+    public ZTSAccessToken(final String token, KeyStore publicKeyProvider, Authorizer authorizer, Principal principal) {
+
+        try {
+            // make sure the required arguments are provided
+
+            if (token == null || publicKeyProvider == null || authorizer == null || principal == null) {
+                throw new CryptoException("Invalid arguments: missing token, public key provider, authorizer or principal");
+            }
+
+            // first parse the token to extract the fields from the body and header
+
+            SignedJWT signedJWT = SignedJWT.parse(token);
+            claimsSet = signedJWT.getJWTClaimsSet();
+
+            // extract the audience of the token which is our domain name
+
+            final String audience = getClaimAudience();
+            if (StringUtils.isEmpty(audience)) {
+                throw new CryptoException("Invalid token: missing audience");
+            }
+
+            // authorize our introspect request. The action is introspect and the
+            // resource is the audience + ":token".
+
+            if (!authorizer.access(ACTION_INTROSPECT, audience + RESOURCE_TOKEN, principal, null)) {
+                throw new CryptoException("unauthorized introspect request");
+            }
+
+            // verify the claims (expiry, not before, etc.)
+
+            claimsVerifier.verify(claimsSet, null);
+
+            // extract the key id from the header
+
+            JWSHeader header = signedJWT.getHeader();
+            String keyId = header.getKeyID();
+            if (StringUtils.isEmpty(keyId)) {
+                throw new CryptoException("Invalid token: missing key id");
+            }
+
+            // get the public key for the zts server for the given key id
+            // and create a verifier
+
+            final PublicKey publicKey = publicKeyProvider.getServicePublicKey(SYS_AUTH_DOMAIN, ZTS_SERVICE, keyId);
+            if (publicKey == null) {
+                throw new CryptoException("Invalid token: unable to get public key");
+            }
+
+            JWSVerifier verifier = JwtsHelper.getJWSVerifier(publicKey);
+            if (!signedJWT.verify(verifier)) {
+                throw new CryptoException("Unable to verify token signature");
+            }
+
+            // if we got this far then we have a valid token so
+            // we'll set our claims set and extract the fields
+
+            setTokenFields();
+            setAccessTokenFields();
+
+        } catch (Exception ex) {
+            throw new CryptoException("Unable to parse token: " + ex.getMessage());
+        }
+    }
+}
diff --git a/libs/java/cert_refresher/pom.xml b/libs/java/cert_refresher/pom.xml
index 164a65b88..f01dcb8c0 100644
--- a/libs/java/cert_refresher/pom.xml
+++ b/libs/java/cert_refresher/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/client_common/pom.xml b/libs/java/client_common/pom.xml
index e8b60aa3b..6f1029e30 100644
--- a/libs/java/client_common/pom.xml
+++ b/libs/java/client_common/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/dynamodb_client_factory/pom.xml b/libs/java/dynamodb_client_factory/pom.xml
index c11fa528d..ef50e4fc3 100644
--- a/libs/java/dynamodb_client_factory/pom.xml
+++ b/libs/java/dynamodb_client_factory/pom.xml
@@ -23,7 +23,7 @@
     <parent>
         <groupId>com.yahoo.athenz</groupId>
         <artifactId>athenz</artifactId>
-        <version>1.12.11-SNAPSHOT</version>
+        <version>1.12.12-SNAPSHOT</version>
         <relativePath>../../../pom.xml</relativePath>
     </parent>
 
diff --git a/libs/java/gcp_zts_creds/pom.xml b/libs/java/gcp_zts_creds/pom.xml
index 2b87fcba6..0685efbe5 100644
--- a/libs/java/gcp_zts_creds/pom.xml
+++ b/libs/java/gcp_zts_creds/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/instance_provider/pom.xml b/libs/java/instance_provider/pom.xml
index dc00c2083..47aed24d0 100644
--- a/libs/java/instance_provider/pom.xml
+++ b/libs/java/instance_provider/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/server_aws_common/pom.xml b/libs/java/server_aws_common/pom.xml
index 5661c167c..59dfe5c3d 100644
--- a/libs/java/server_aws_common/pom.xml
+++ b/libs/java/server_aws_common/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/server_common/pom.xml b/libs/java/server_common/pom.xml
index 10a9c4c4d..317cb01e2 100644
--- a/libs/java/server_common/pom.xml
+++ b/libs/java/server_common/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommon.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommon.java
index 06633a035..df4d456f0 100644
--- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommon.java
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommon.java
@@ -68,7 +68,8 @@ public class NotificationConverterCommon {
     private final String userDomainPrefixRegex;
     private final String userDomainPrefix;
     private final String emailDomainTo;
-    private final String workflowUrl;
+    private final String adminWorkflowUrl;
+    private final String domainWorkflowUrl;
     private final String athenzUIUrl;
     private final String supportText;
     private final String supportUrl;
@@ -81,8 +82,9 @@ public class NotificationConverterCommon {
         userDomainPrefixRegex = userDomain + "\\.";
         userDomainPrefix = userDomain + ".";
         emailDomainTo = System.getProperty(PROP_NOTIFICATION_EMAIL_DOMAIN_TO);
-        workflowUrl = System.getProperty(PROP_NOTIFICATION_WORKFLOW_URL);
+        adminWorkflowUrl = System.getProperty(PROP_NOTIFICATION_WORKFLOW_URL);
         athenzUIUrl = System.getProperty(PROP_NOTIFICATION_ATHENZ_UI_URL);
+        domainWorkflowUrl = athenzUIUrl + "/workflow/domain";
         supportText = System.getProperty(PROP_NOTIFICATION_SUPPORT_TEXT);
         supportUrl = System.getProperty(PROP_NOTIFICATION_SUPPORT_URL);
 
@@ -278,15 +280,17 @@ public class NotificationConverterCommon {
         return userName.replaceAll(userDomainPrefixRegex, "") + AT + emailDomainTo;
     }
 
-    public String getWorkflowUrl() {
-        return workflowUrl == null ? "" : workflowUrl;
+    public String getAdminWorkflowUrl() {
+        return adminWorkflowUrl == null ? "" : adminWorkflowUrl;
     }
 
-    public String getWorkflowUrl(String domainName) {
-        if (workflowUrl == null) {
+
+    public String getDomainWorkflowUrl(String domainName) {
+        // check for athenzUI url since domainWorkflowUrl is based on it
+        if (athenzUIUrl == null) {
             return "";
         }
-        return StringUtil.isEmpty(domainName) ? workflowUrl : workflowUrl + "/domain?domain=" + domainName;
+        return StringUtil.isEmpty(domainName) ? domainWorkflowUrl : domainWorkflowUrl + "?domain=" + domainName;
     }
 
     public String getAthenzUIUrl() {
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/ServerResourceContext.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/ServerResourceContext.java
index 86bb51dda..cb9261543 100644
--- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/ServerResourceContext.java
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/rest/ServerResourceContext.java
@@ -182,4 +182,12 @@ public class ServerResourceContext {
     public void setRequestDomain(String requestDomain) {
         this.requestDomain = requestDomain;
     }
+
+    /**
+     * Set the principal object
+     * @param principal Principal object
+     */
+    public void setPrincipal(Principal principal) {
+        this.principal = principal;
+    }
 }
diff --git a/libs/java/server_k8s_common/pom.xml b/libs/java/server_k8s_common/pom.xml
index cda0964c4..8362cfde1 100644
--- a/libs/java/server_k8s_common/pom.xml
+++ b/libs/java/server_k8s_common/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/server_msg_pulsar/pom.xml b/libs/java/server_msg_pulsar/pom.xml
index c3e4bd69a..a9f9a9c5b 100644
--- a/libs/java/server_msg_pulsar/pom.xml
+++ b/libs/java/server_msg_pulsar/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/libs/java/server_notification_slack/pom.xml b/libs/java/server_notification_slack/pom.xml
index 8d77613cf..ded0ec789 100644
--- a/libs/java/server_notification_slack/pom.xml
+++ b/libs/java/server_notification_slack/pom.xml
@@ -17,7 +17,7 @@
     <parent>
         <groupId>com.yahoo.athenz</groupId>
         <artifactId>athenz</artifactId>
-        <version>1.12.11-SNAPSHOT</version>
+        <version>1.12.12-SNAPSHOT</version>
         <relativePath>../../../pom.xml</relativePath>
     </parent>
 
diff --git a/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/SlackNotificationConsts.java b/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/SlackNotificationConsts.java
index 259aa7533..d168dbad7 100644
--- a/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/SlackNotificationConsts.java
+++ b/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/SlackNotificationConsts.java
@@ -25,6 +25,7 @@ public final class SlackNotificationConsts {
     public static final String PROP_SLACK_FETCH_TOKEN_PERIOD_BETWEEN_EXECUTIONS = "athenz.notification_slack.period_between_fetch_token_executions_seconds";
     public static final String DEFAULT_SLACK_FETCH_TOKEN_PERIOD_BETWEEN_EXECUTIONS = "3600";
     public static final String SLACK_API_INVALID_AUTH_ERROR = "invalid_auth";
+    public static final String SLACK_API_TOKEN_EXPIRED_ERROR = "token_expired";
     public static final String SLACK_API_RATE_LIMIT_ERROR = "rate_limited";
     public static final Integer SLACK_API_RATE_LIMIT_CODE = 429;
     // prevent object creation
diff --git a/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClient.java b/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClient.java
index ece5ffdd9..d9620484a 100644
--- a/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClient.java
+++ b/libs/java/server_notification_slack/src/main/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClient.java
@@ -22,6 +22,7 @@ import com.yahoo.athenz.auth.PrivateKeyStore;
 import com.slack.api.Slack;
 import com.slack.api.methods.request.chat.ChatPostMessageRequest;
 import com.slack.api.methods.response.chat.ChatPostMessageResponse;
+import org.eclipse.jetty.util.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -192,7 +193,7 @@ public class AthenzSlackClient {
             return false;
         }
 
-        if (SLACK_API_INVALID_AUTH_ERROR.equals(error)) {
+        if (SLACK_API_INVALID_AUTH_ERROR.equals(error) || SLACK_API_TOKEN_EXPIRED_ERROR.equals(error)) {
             LOGGER.warn("Token expired during {}, refreshing and retrying... ({} retries left)",
                     operation, retriesLeft - 1);
             refreshToken();
@@ -212,6 +213,12 @@ public class AthenzSlackClient {
             }
         }
 
+        // try refreshing token if error doesn't match but isn't empty
+        if (!StringUtil.isEmpty(error)) {
+            refreshToken();
+            return true;
+        }
+
         return false;
     }
 
diff --git a/libs/java/syncer_common/pom.xml b/libs/java/syncer_common/pom.xml
index 2fae1a55a..97a311c75 100644
--- a/libs/java/syncer_common/pom.xml
+++ b/libs/java/syncer_common/pom.xml
@@ -23,7 +23,7 @@
     <parent>
         <groupId>com.yahoo.athenz</groupId>
         <artifactId>athenz</artifactId>
-        <version>1.12.11-SNAPSHOT</version>
+        <version>1.12.12-SNAPSHOT</version>
         <relativePath>../../../pom.xml</relativePath>
     </parent>
 
diff --git a/libs/nodejs/auth_core/pom.xml b/libs/nodejs/auth_core/pom.xml
index acf3ffb58..54f2ebe62 100644
--- a/libs/nodejs/auth_core/pom.xml
+++ b/libs/nodejs/auth_core/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/pom.xml b/pom.xml
index e6d78b0ab..6719eba5f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
   <groupId>com.yahoo.athenz</groupId>
   <artifactId>athenz</artifactId>
   <packaging>pom</packaging>
-  <version>1.12.11-SNAPSHOT</version>
+  <version>1.12.12-SNAPSHOT</version>
 
   <name>AthenZ</name>
   <description>
@@ -74,8 +74,8 @@
     <angus.mail.version>2.0.3</angus.mail.version>
     <apache.httpclient.version>5.4.2</apache.httpclient.version>
     <apache.httpclient4.version>4.5.14</apache.httpclient4.version>
-    <aws.version>1.12.780</aws.version>
-    <aws2.version>2.30.16</aws2.version>
+    <aws.version>1.12.782</aws.version>
+    <aws2.version>2.30.25</aws2.version>
     <aws.dynamodb.local.version>2.5.4</aws.dynamodb.local.version>
     <aws.secretmanager.version>2.0.2</aws.secretmanager.version>
     <bouncycastle.version>1.80</bouncycastle.version>
@@ -83,7 +83,7 @@
     <commons.daemon.version>1.4.1</commons.daemon.version>
     <commons.dbcp2.version>2.13.0</commons.dbcp2.version>
     <commons.io.version>2.18.0</commons.io.version>
-    <commons.logging.version>1.3.4</commons.logging.version>
+    <commons.logging.version>1.3.5</commons.logging.version>
     <commons.pool2.version>2.12.1</commons.pool2.version>
     <ehcache.version>3.10.8</ehcache.version>
     <freemarker.version>2.3.34</freemarker.version>
@@ -107,7 +107,7 @@
     <jmh.version>1.35</jmh.version>
     <jna.version>5.16.0</jna.version>
     <junit.version>4.13.2</junit.version>
-    <kubernetes-client.version>22.0.0</kubernetes-client.version>
+    <kubernetes-client.version>23.0.0</kubernetes-client.version>
     <logback.client.version>1.2.13</logback.client.version>
     <logback.server.version>1.5.16</logback.server.version>
     <mock.server.version>5.15.0</mock.server.version>
@@ -120,13 +120,13 @@
     <protobuf.java.version>4.29.3</protobuf.java.version>
     <pulsar.client.version>4.0.2</pulsar.client.version>
     <rdl.version>1.5.4</rdl.version>
-    <slack.client.version>1.45.2</slack.client.version>
+    <slack.client.version>1.45.3</slack.client.version>
     <slf4j.client.version>1.7.36</slf4j.client.version>
     <slf4j.server.version>2.0.16</slf4j.server.version>
     <sqlite4java.version>1.0.392</sqlite4java.version>
     <swagger.version>2.2.28</swagger.version>
-    <testcontainers.version>1.20.4</testcontainers.version>
-    <testng.version>7.10.2</testng.version>
+    <testcontainers.version>1.20.5</testcontainers.version>
+    <testng.version>7.11.0</testng.version>
     <uuid.version>5.1.0</uuid.version>
 
     <maven-assembly-plugin.version>3.7.1</maven-assembly-plugin.version>
diff --git a/provider/aws/sia-ec2/pom.xml b/provider/aws/sia-ec2/pom.xml
index 5859568eb..3f8b3e7c8 100644
--- a/provider/aws/sia-ec2/pom.xml
+++ b/provider/aws/sia-ec2/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/aws/sia-eks/pom.xml b/provider/aws/sia-eks/pom.xml
index 9b6ae0f7f..53ce9ed70 100644
--- a/provider/aws/sia-eks/pom.xml
+++ b/provider/aws/sia-eks/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/aws/sia-fargate/pom.xml b/provider/aws/sia-fargate/pom.xml
index f9283a749..6a5199d30 100644
--- a/provider/aws/sia-fargate/pom.xml
+++ b/provider/aws/sia-fargate/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/azure/sia-vm/pom.xml b/provider/azure/sia-vm/pom.xml
index 9bc2e0ab6..e98ed1cc6 100644
--- a/provider/azure/sia-vm/pom.xml
+++ b/provider/azure/sia-vm/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/buildkite/sia-buildkite/pom.xml b/provider/buildkite/sia-buildkite/pom.xml
index 5178c62f4..558a45d9d 100644
--- a/provider/buildkite/sia-buildkite/pom.xml
+++ b/provider/buildkite/sia-buildkite/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/gcp/sia-gce/pom.xml b/provider/gcp/sia-gce/pom.xml
index a01785586..3350e40a0 100644
--- a/provider/gcp/sia-gce/pom.xml
+++ b/provider/gcp/sia-gce/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/gcp/sia-gke/pom.xml b/provider/gcp/sia-gke/pom.xml
index 19f914717..6cbdc1df8 100644
--- a/provider/gcp/sia-gke/pom.xml
+++ b/provider/gcp/sia-gke/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/gcp/sia-run/pom.xml b/provider/gcp/sia-run/pom.xml
index bcd7e0e18..cf70771d5 100644
--- a/provider/gcp/sia-run/pom.xml
+++ b/provider/gcp/sia-run/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/github/sia-actions/pom.xml b/provider/github/sia-actions/pom.xml
index 761798929..43ffb55d6 100644
--- a/provider/github/sia-actions/pom.xml
+++ b/provider/github/sia-actions/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/provider/harness/sia-harness/pom.xml b/provider/harness/sia-harness/pom.xml
index de317ed17..ca61b9351 100644
--- a/provider/harness/sia-harness/pom.xml
+++ b/provider/harness/sia-harness/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../../pom.xml</relativePath>
   </parent>
 
diff --git a/rdl/rdl-gen-athenz-go-client/pom.xml b/rdl/rdl-gen-athenz-go-client/pom.xml
index 708c34003..bc66fa136 100644
--- a/rdl/rdl-gen-athenz-go-client/pom.xml
+++ b/rdl/rdl-gen-athenz-go-client/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/rdl/rdl-gen-athenz-go-model/pom.xml b/rdl/rdl-gen-athenz-go-model/pom.xml
index 540db8416..01bc32dbe 100644
--- a/rdl/rdl-gen-athenz-go-model/pom.xml
+++ b/rdl/rdl-gen-athenz-go-model/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/rdl/rdl-gen-athenz-java-client/pom.xml b/rdl/rdl-gen-athenz-java-client/pom.xml
index ff72d5b9a..64f0a40c0 100644
--- a/rdl/rdl-gen-athenz-java-client/pom.xml
+++ b/rdl/rdl-gen-athenz-java-client/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/rdl/rdl-gen-athenz-java-model/pom.xml b/rdl/rdl-gen-athenz-java-model/pom.xml
index 01cf67c28..db261043a 100644
--- a/rdl/rdl-gen-athenz-java-model/pom.xml
+++ b/rdl/rdl-gen-athenz-java-model/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/rdl/rdl-gen-athenz-server/pom.xml b/rdl/rdl-gen-athenz-server/pom.xml
index ad9d4e26e..14a3f742c 100644
--- a/rdl/rdl-gen-athenz-server/pom.xml
+++ b/rdl/rdl-gen-athenz-server/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/servers/zms/pom.xml b/servers/zms/pom.xml
index 17a871b6e..78d75582a 100644
--- a/servers/zms/pom.xml
+++ b/servers/zms/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
index 01fd81f16..2a362f27c 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
@@ -80,6 +80,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.security.PublicKey;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
@@ -9254,6 +9255,18 @@ public class ZMSImpl implements Authorizer, KeyStore, ZMSHandler {
         return (SYS_AUTH.equalsIgnoreCase(domain) && ZMSConsts.ZMS_SERVICE.equalsIgnoreCase(service));
     }
 
+    /**
+     * implements KeyStore getServicePublicKey
+     * @return PublicKey object
+     **/
+
+    @Override
+    public PublicKey getServicePublicKey(String domain, String service, String keyId) {
+
+        final String publicKeyPem = getPublicKey(domain, service, keyId);
+        return (publicKeyPem == null) ? null : Crypto.loadPublicKey(publicKeyPem);
+    }
+
     /**
      * implements KeyStore getPublicKey
      * @return String with PEM encoded key, which should be ybase64decoded prior
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingGroupMembershipApprovalNotificationTask.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingGroupMembershipApprovalNotificationTask.java
index a0bf493de..725036d9b 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingGroupMembershipApprovalNotificationTask.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingGroupMembershipApprovalNotificationTask.java
@@ -92,7 +92,7 @@ public class PendingGroupMembershipApprovalNotificationTask implements Notificat
         }
 
         private String getMembershipApprovalReminderBody() {
-            String workflowUrl = notificationConverterCommon.getWorkflowUrl();
+            String workflowUrl = notificationConverterCommon.getAdminWorkflowUrl();
             String athenzUIUrl = notificationConverterCommon.getAthenzUIUrl();
             String body = MessageFormat.format(emailMembershipApprovalReminderBody, workflowUrl, athenzUIUrl);
             return notificationConverterCommon.addCssStyleToBody(body);
@@ -136,7 +136,7 @@ public class PendingGroupMembershipApprovalNotificationTask implements Notificat
 
         private String getMembershipApprovalReminderSlackMessage() {
             Map<String, Object> dataModel = new HashMap<>();
-            dataModel.put("workflowLink", notificationConverterCommon.getWorkflowUrl());
+            dataModel.put("workflowLink", notificationConverterCommon.getAdminWorkflowUrl());
             return notificationConverterCommon.generateSlackMessageFromTemplate(
                     dataModel,
                     slackMembershipApprovalReminderTemplate);
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingRoleMembershipApprovalNotificationTask.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingRoleMembershipApprovalNotificationTask.java
index f7ae69b9c..e6c088af8 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingRoleMembershipApprovalNotificationTask.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PendingRoleMembershipApprovalNotificationTask.java
@@ -57,25 +57,26 @@ public class PendingRoleMembershipApprovalNotificationTask implements Notificati
         // will have both audit roles and admin roles of domain
         Set<String> recipients = dbService.getPendingMembershipApproverRoles(1);
 
-        Notification consolidateByPrincipalNotification = notificationCommon.createNotification(
+        List<Notification> notificationList = new ArrayList<>();
+        notificationList.add(notificationCommon.createNotification(
                 Notification.Type.PENDING_ROLE_APPROVAL,
                 Notification.ConsolidatedBy.PRINCIPAL,
                 recipients,
                 null,
                 pendingMembershipApprovalNotificationToEmailConverter,
                 pendingRoleMembershipApprovalNotificationToMetricConverter,
-                pendingRoleMembershipApprovalNotificationToSlackMessageConverter);
-
-        Notification consolidatedByDomainNotification = notificationCommon.createNotification(
+                pendingRoleMembershipApprovalNotificationToSlackMessageConverter));
+        notificationList.add(notificationCommon.createNotification(
                 Notification.Type.PENDING_ROLE_APPROVAL,
                 Notification.ConsolidatedBy.DOMAIN,
                 recipients,
                 null,
                 pendingMembershipApprovalNotificationToEmailConverter,
                 pendingRoleMembershipApprovalNotificationToMetricConverter,
-                pendingRoleMembershipApprovalNotificationToSlackMessageConverter);
+                pendingRoleMembershipApprovalNotificationToSlackMessageConverter));
+
 
-        return List.of(consolidateByPrincipalNotification, consolidatedByDomainNotification);
+        return notificationList;
     }
 
     @Override
@@ -96,7 +97,7 @@ public class PendingRoleMembershipApprovalNotificationTask implements Notificati
         }
 
         private String getMembershipApprovalReminderBody() {
-            String workflowUrl = notificationConverterCommon.getWorkflowUrl();
+            String workflowUrl = notificationConverterCommon.getAdminWorkflowUrl();
             String athenzUIUrl = notificationConverterCommon.getAthenzUIUrl();
             String body = MessageFormat.format(emailMembershipApprovalReminderBody, workflowUrl, athenzUIUrl);
             return notificationConverterCommon.addCssStyleToBody(body);
@@ -141,7 +142,7 @@ public class PendingRoleMembershipApprovalNotificationTask implements Notificati
 
         private String getMembershipApprovalReminderSlackMessage() {
             Map<String, Object> dataModel = new HashMap<>();
-            dataModel.put("workflowLink", notificationConverterCommon.getWorkflowUrl());
+            dataModel.put("workflowLink", notificationConverterCommon.getAdminWorkflowUrl());
             return notificationConverterCommon.generateSlackMessageFromTemplate(
                     dataModel,
                     slackMembershipApprovalReminderTemplate);
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutGroupMembershipNotificationTask.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutGroupMembershipNotificationTask.java
index 02d17d911..fa90ed88e 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutGroupMembershipNotificationTask.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutGroupMembershipNotificationTask.java
@@ -108,7 +108,7 @@ public class PutGroupMembershipNotificationTask implements NotificationTask {
             if (metaDetails == null) {
                 return null;
             }
-            String workflowUrl = notificationConverterCommon.getWorkflowUrl();
+            String workflowUrl = notificationConverterCommon.getAdminWorkflowUrl();
             String athenzUIUrl = notificationConverterCommon.getAthenzUIUrl();
             String body = MessageFormat.format(emailMembershipApprovalBody, metaDetails.get(NOTIFICATION_DETAILS_DOMAIN),
                     metaDetails.get(NOTIFICATION_DETAILS_GROUP), metaDetails.get(NOTIFICATION_DETAILS_MEMBER),
@@ -164,7 +164,7 @@ public class PutGroupMembershipNotificationTask implements NotificationTask {
             metaDetails.forEach(dataModel::put);
             dataModel.put("domainLink", notificationConverterCommon.getDomainLink(domainName));
             dataModel.put("groupLink", notificationConverterCommon.getGroupLink(domainName, groupName));
-            dataModel.put("workflowLink", notificationConverterCommon.getWorkflowUrl(domainName));
+            dataModel.put("workflowLink", notificationConverterCommon.getDomainWorkflowUrl(domainName));
 
             return notificationConverterCommon.generateSlackMessageFromTemplate(
                     dataModel,
diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutRoleMembershipNotificationTask.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutRoleMembershipNotificationTask.java
index 59bc66d6e..cfac2dbbb 100644
--- a/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutRoleMembershipNotificationTask.java
+++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/notification/PutRoleMembershipNotificationTask.java
@@ -107,7 +107,7 @@ public class PutRoleMembershipNotificationTask implements NotificationTask {
             if (metaDetails == null) {
                 return null;
             }
-            String workflowUrl = notificationConverterCommon.getWorkflowUrl();
+            String workflowUrl = notificationConverterCommon.getAdminWorkflowUrl();
             String athenzUIUrl = notificationConverterCommon.getAthenzUIUrl();
             String body = MessageFormat.format(emailMembershipApprovalBody, metaDetails.get(NOTIFICATION_DETAILS_DOMAIN),
                     metaDetails.get(NOTIFICATION_DETAILS_ROLE), metaDetails.get(NOTIFICATION_DETAILS_MEMBER),
@@ -163,7 +163,7 @@ public class PutRoleMembershipNotificationTask implements NotificationTask {
             metaDetails.forEach(dataModel::put);
             dataModel.put("domainLink", notificationConverterCommon.getDomainLink(domainName));
             dataModel.put("roleLink", notificationConverterCommon.getRoleLink(domainName, roleName));
-            dataModel.put("workflowLink", notificationConverterCommon.getWorkflowUrl(domainName));
+            dataModel.put("workflowLink", notificationConverterCommon.getDomainWorkflowUrl(domainName));
 
             return notificationConverterCommon.generateSlackMessageFromTemplate(
                     dataModel,
diff --git a/servers/zts/conf/zts.properties b/servers/zts/conf/zts.properties
index 172c0f7db..85832b4fe 100644
--- a/servers/zts/conf/zts.properties
+++ b/servers/zts/conf/zts.properties
@@ -832,3 +832,7 @@ athenz.zts.k8s_provider_distribution_validator_factory_class=com.yahoo.athenz.in
 # This property specifies whether the server should cache all errors from
 # AWS STS service or just the errors that return forbidden (403) status code.
 #athenz.zts.aws_creds_invalid_cache_all_errors=true
+
+# This property specifies whether the /zts/v1/oauth2/introspect endpoint should
+# be enabled or not. By default, the introspection endpoint is disabled.
+#athenz.zts.introspect_support_enabled=false
diff --git a/servers/zts/pom.xml b/servers/zts/pom.xml
index 8d6064b78..3a923ee15 100644
--- a/servers/zts/pom.xml
+++ b/servers/zts/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
@@ -29,7 +29,7 @@
   <packaging>war</packaging>
 
   <properties>
-    <code.coverage.min>0.9932</code.coverage.min>
+    <code.coverage.min>0.9934</code.coverage.min>
   </properties>
 
   <dependencies>
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/RsrcCtxWrapper.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/RsrcCtxWrapper.java
index 7298e4904..46d415368 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/RsrcCtxWrapper.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/RsrcCtxWrapper.java
@@ -193,4 +193,8 @@ public class RsrcCtxWrapper implements ResourceContext {
 
         throw ZTSUtils.error(restExc);
     }
+
+    public void setPrincipal(Principal principal) {
+        ctx.setPrincipal(principal);
+    }
 }
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java
index 8b68d6b8e..b1052cd05 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSConsts.java
@@ -97,6 +97,7 @@ public final class ZTSConsts {
     public static final String ZTS_PROP_AUTHORIZED_PROXY_USERS     = "athenz.zts.authorized_proxy_users";
     public static final String ZTS_PROP_SECURE_REQUESTS_ONLY       = "athenz.zts.secure_requests_only";
     public static final String ZTS_PROP_STATUS_CERT_SIGNER         = "athenz.zts.status_cert_signer";
+    public static final String ZTS_PROP_INTROSPECT_SUPPORT_ENABLED = "athenz.zts.introspect_support_enabled";
 
     public static final String ZTS_PROP_SELF_SIGNER_PRIVATE_KEY_FNAME    = "athenz.zts.self_signer_private_key_fname";
     public static final String ZTS_PROP_SELF_SIGNER_PRIVATE_KEY_PASSWORD = "athenz.zts.self_signer_private_key_password";
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSHandler.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSHandler.java
index 16e4c6e07..bf3c2fc48 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSHandler.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSHandler.java
@@ -40,6 +40,7 @@ public interface ZTSHandler {
     JWKList getJWKList(ResourceContext context, Boolean rfc, String service);
     AccessTokenResponse postAccessTokenRequest(ResourceContext context, String request);
     Response getOIDCResponse(ResourceContext context, String responseType, String clientId, String redirectUri, String scope, String state, String nonce, String keyType, Boolean fullArn, Integer expiryTime, String output, Boolean roleInAudClaim, Boolean allScopePresent);
+    IntrospectResponse postIntrospectRequest(ResourceContext context, String request);
     RoleCertificate postRoleCertificateRequestExt(ResourceContext context, RoleCertificateRequest req);
     RoleAccess getRolesRequireRoleCert(ResourceContext context, String principal);
     Workloads getWorkloadsByService(ResourceContext context, String domainName, String serviceName);
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java
index b07db28e3..661b91ce0 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSImpl.java
@@ -23,6 +23,7 @@ import com.yahoo.athenz.auth.impl.SimplePrincipal;
 import com.yahoo.athenz.auth.token.AccessToken;
 import com.yahoo.athenz.auth.token.IdToken;
 import com.yahoo.athenz.auth.token.PrincipalToken;
+import com.yahoo.athenz.auth.token.ZTSAccessToken;
 import com.yahoo.athenz.auth.util.AthenzUtils;
 import com.yahoo.athenz.auth.util.Crypto;
 import com.yahoo.athenz.auth.util.CryptoException;
@@ -73,9 +74,9 @@ import com.yahoo.athenz.zts.cert.*;
 import com.yahoo.athenz.zts.notification.ZTSNotificationTaskFactory;
 import com.yahoo.athenz.zts.store.CloudStore;
 import com.yahoo.athenz.zts.store.DataStore;
-import com.yahoo.athenz.zts.token.AccessTokenBody;
 import com.yahoo.athenz.zts.token.AccessTokenRequest;
-import com.yahoo.athenz.zts.token.IdTokenRequest;
+import com.yahoo.athenz.zts.token.AccessTokenScope;
+import com.yahoo.athenz.zts.token.IdTokenScope;
 import com.yahoo.athenz.zts.transportrules.TransportRulesProcessor;
 import com.yahoo.athenz.zts.utils.ZTSUtils;
 import com.yahoo.rdl.*;
@@ -112,7 +113,7 @@ import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.
 /**
  * An implementation of ZTS.
  */
-public class ZTSImpl implements KeyStore, ZTSHandler {
+public class ZTSImpl implements ZTSHandler {
 
     private static String ROOT_DIR;
 
@@ -161,6 +162,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
     protected boolean statusCertSigner = false;
     protected Status successServerStatus = null;
     protected boolean includeRoleCompleteFlag = true;
+    protected boolean introspectSupportEnabled = false;
     protected DynamicConfigBoolean readOnlyMode;
     protected boolean verifyCertRequestIP = false;
     protected boolean verifyCertSubjectOU = false;
@@ -208,14 +210,8 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
     private static final long ZTS_NTOKEN_DEFAULT_EXPIRY = TimeUnit.SECONDS.convert(2, TimeUnit.HOURS);
     private static final long ZTS_NTOKEN_MAX_EXPIRY = TimeUnit.SECONDS.convert(7, TimeUnit.DAYS);
 
-    private static final String KEY_SCOPE = "scope";
-    private static final String KEY_GRANT_TYPE = "grant_type";
-    private static final String KEY_EXPIRES_IN = "expires_in";
-    private static final String KEY_PROXY_FOR_PRINCIPAL = "proxy_for_principal";
-    private static final String KEY_AUTHORIZATION_DETAILS = "authorization_details";
-    private static final String KEY_PROXY_PRINCIPAL_SPIFFE_URIS = "proxy_principal_spiffe_uris";
-    private static final String KEY_OPENID_ISSUER = "openid_issuer";
     private static final String KEY_TYPE = "type";
+    private static final String KEY_TOKEN = "token";
 
     private static final String OAUTH_GRANT_CREDENTIALS = "client_credentials";
     private static final String OAUTH_BEARER_TOKEN = "Bearer";
@@ -365,7 +361,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         instanceProviderManager = new InstanceProviderManager(dataStore,
                 ZTSUtils.getAthenzServerSSLContext(privateKeyStore),
                 ZTSUtils.getAthenzProviderClientSSLContext(privateKeyStore),
-                getServerPrivateKey(keyAlgoForInstanceProviders), this, authorizer, this);
+                getServerPrivateKey(keyAlgoForInstanceProviders), dataStore, authorizer, this);
 
         // make sure to set the keystore for any instance that requires it
 
@@ -463,6 +459,9 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         oauthConfig = new OAuthConfig();
         oauthConfig.setIssuer(ztsOpenIDIssuer);
         oauthConfig.setJwks_uri(ztsOpenIDIssuer + "/oauth2/keys?rfc=true");
+        if (introspectSupportEnabled) {
+            oauthConfig.setIntrospection_endpoint(ztsOpenIDIssuer + "/oauth2/introspect");
+        }
         oauthConfig.setAuthorization_endpoint(ztsOpenIDIssuer + "/oauth2/auth");
         oauthConfig.setToken_endpoint(ztsOpenIDIssuer + "/oauth2/token");
         oauthConfig.setGrant_types_supported(Collections.singletonList(OAUTH_GRANT_CREDENTIALS));
@@ -722,6 +721,11 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
 
         serviceCertDefaultExpiryMins = new DynamicConfigInteger(CONFIG_MANAGER,
                 ZTSConsts.ZTS_PROP_SERVICE_CERT_DEFAULT_EXPIRY_MINS, 0);
+
+        // check if introspect support is enabled
+
+        introspectSupportEnabled = Boolean.parseBoolean(
+                System.getProperty(ZTSConsts.ZTS_PROP_INTROSPECT_SUPPORT_ENABLED, "false"));
     }
 
     static String getServerHostName() {
@@ -743,7 +747,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
     void setAuthorityKeyStore() {
         for (Authority authority : authorities.getAuthorities()) {
             if (authority instanceof AuthorityKeyStore) {
-                ((AuthorityKeyStore) authority).setKeyStore(this);
+                ((AuthorityKeyStore) authority).setKeyStore(dataStore);
             }
         }
     }
@@ -905,26 +909,6 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         }
     }
 
-    @Override
-    public String getPublicKey(String domain, String service, String keyId) {
-
-        // for consistent handling of all requests, we're going to convert
-        // all incoming object values into lower case since ZMS Server
-        // saves all of its object names in lower case
-
-        if (domain != null) {
-            domain = domain.toLowerCase();
-        }
-        if (service != null) {
-            service = service.toLowerCase();
-        }
-        if (keyId != null) {
-            keyId = keyId.toLowerCase();
-        }
-
-        return dataStore.getPublicKey(domain, service, keyId);
-    }
-
     ServiceIdentity generateZTSServiceIdentity(com.yahoo.athenz.zms.ServiceIdentity zmsService) {
 
         // zms and zts are using the same definition for service identities but
@@ -1947,19 +1931,12 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // caller is authorized for such operations
 
         if (!isAuthorizedProxyUser(authorizedProxyUsers, principalName)) {
-            LOGGER.error("postAccessTokenRequest: Principal {} not authorized for proxy role token request", principalName);
-            throw forbiddenError("postAccessTokenRequest: Principal: " + principalName
-                    + " not authorized for proxy access token request", caller,
-                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+            LOGGER.error("Principal {} not authorized for proxy role token request", principalName);
+            throw forbiddenError("Principal: " + principalName + " not authorized for proxy access token request",
+                    caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
         }
     }
 
-    String getQueryLogData(final String request) {
-        // make sure any CRLFs are not set to the logger
-        final String clean = request.replace('\n', '_').replace('\r', '_');
-        return (clean.length() > 1024) ? clean.substring(0, 1024) : clean;
-    }
-
     @Override
     public Response getOIDCResponse(ResourceContext ctx, String responseType, String clientId, String redirectUri,
             String scope, String state, String nonce, String keyType, Boolean fullArn,
@@ -2028,7 +2005,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // our scopes are space separated list of values. Any groups
         // scopes are preferred over any role scopes
 
-        IdTokenRequest tokenRequest = new IdTokenRequest(scope);
+        IdTokenScope tokenScope = new IdTokenScope(scope);
 
         // check if the authorized service domain matches to the
         // requested domain name
@@ -2045,14 +2022,14 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // either groups or roles for our response
 
         List<String> idTokenGroups = null;
-        if (tokenRequest.isGroupsScope()) {
+        if (tokenScope.isGroupsScope()) {
 
-            idTokenGroups = processIdTokenGroups(principalName, tokenRequest, domainName,
+            idTokenGroups = processIdTokenGroups(principalName, tokenScope, domainName,
                     allScopePresent, principalDomain, caller);
 
-        } else if (tokenRequest.isRolesScope()) {
+        } else if (tokenScope.isRolesScope()) {
 
-            idTokenGroups = processIdTokenRoles(principalName, tokenRequest, domainName, fullArn,
+            idTokenGroups = processIdTokenRoles(principalName, tokenScope, domainName, fullArn,
                     allScopePresent, principalDomain, caller);
         }
 
@@ -2110,7 +2087,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
                 clientId + ":" + idTokenGroups.get(0) : clientId;
     }
 
-    List<String> processIdTokenGroups(final String principalName, IdTokenRequest tokenRequest,
+    List<String> processIdTokenGroups(final String principalName, IdTokenScope tokenRequest,
             final String clientIdDomainName, Boolean allScopePresent,
             final String principalDomain, final String caller) {
 
@@ -2183,7 +2160,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         return getIdTokenGroupsFromGroups(groups, domainName);
     }
 
-    List<String> processIdTokenRoles(final String principalName, IdTokenRequest tokenRequest,
+    List<String> processIdTokenRoles(final String principalName, IdTokenScope tokenRequest,
             final String clientIdDomainName, Boolean fullArn, Boolean allScopePresent,
             final String principalDomain, final String caller) {
 
@@ -2389,60 +2366,156 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         return serverPrivateKey;
     }
 
+    String extractTokenValue(final String request) {
+
+        if (StringUtil.isEmpty(request)) {
+            return null;
+        }
+
+        String token = null;
+        String[] comps = request.split("&");
+        for (String comp : comps) {
+            int idx = comp.indexOf('=');
+            if (idx == -1) {
+                if (LOGGER.isDebugEnabled()) {
+                    LOGGER.debug("postIntrospectRequest: skipping invalid component: {}", comp);
+                }
+                continue;
+            }
+            final String key = comp.substring(0, idx);
+            final String value = comp.substring(idx + 1);
+            if (KEY_TOKEN.equals(key)) {
+                token = value;
+                break;
+            }
+        }
+
+        return token;
+    }
+
     @Override
-    public AccessTokenResponse postAccessTokenRequest(ResourceContext ctx, String request) {
+    public IntrospectResponse postIntrospectRequest(ResourceContext ctx, String request) {
 
         final String caller = ctx.getApiName();
 
         final String principalDomain = logPrincipalAndGetDomain(ctx);
         validateRequest(ctx.request(), principalDomain, caller);
 
-        // get our principal's name
+        if (!introspectSupportEnabled) {
+            throw requestError("postIntrospectRequest: Introspection is not supported", caller,
+                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+        }
+        // extract the token from the request body
 
-        final Principal principal = ((RsrcCtxWrapper) ctx).principal();
-        String principalName = principal.getFullName();
+        final String token = extractTokenValue(request);
+        if (StringUtil.isEmpty(token)) {
+            throw requestError("postIntrospectRequest: No token provided", caller,
+                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+        }
 
-        if (StringUtil.isEmpty(request)) {
-            throw requestError("Empty request body", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+        ZTSAccessToken accessToken = null;
+        try {
+            accessToken = new ZTSAccessToken(token, dataStore, authorizer, ((RsrcCtxWrapper) ctx).principal());
+        } catch (Exception ex) {
+            LOGGER.error("postIntrospectRequest: Unable to parse token: {}", ex.getMessage());
         }
 
-        // we want to log the request body in our access log so
-        // we know what is the client asking for but we'll just
-        // limit the request up to 1K
+        if (accessToken == null) {
+            return new IntrospectResponse().setActive(false);
+        } else {
+            return new IntrospectResponse().setActive(true)
+                    .setAud(accessToken.getAudience())
+                    .setExp(accessToken.getExpiryTime())
+                    .setIat(accessToken.getIssueTime())
+                    .setIss(accessToken.getIssuer())
+                    .setAuth_time(accessToken.getAuthTime())
+                    .setSub(accessToken.getSubject())
+                    .setScope(accessToken.getScopeStd())
+                    .setVer(accessToken.getVersion())
+                    .setClient_id(accessToken.getClientId())
+                    .setJti(accessToken.getJwtId())
+                    .setUid(accessToken.getUserId())
+                    .setProxy(accessToken.getProxyPrincipal())
+                    .setAuthorization_details(accessToken.getAuthorizationDetails());
+        }
+    }
+
+    @Override
+    public AccessTokenResponse postAccessTokenRequest(ResourceContext ctx, String request) {
+
+        final String caller = ctx.getApiName();
+
+        String principalDomain = logPrincipalAndGetDomain(ctx);
+        Principal principal = ((RsrcCtxWrapper) ctx).principal();
 
-        ctx.request().setAttribute(ACCESS_LOG_ADDL_QUERY, getQueryLogData(request));
+        // if our principal domain is null, then most likely the authentication
+        // is set as optional and the request body contains the jwt token.
+        // we need to set the domain as unknown and will carry out the remaining
+        // checks once we extract the principal from the request body
+
+        if (principalDomain == null) {
+            principalDomain = ZTSConsts.ZTS_UNKNOWN_DOMAIN;
+        }
+
+        validateRequest(ctx.request(), principalDomain, caller);
+
+        if (StringUtil.isEmpty(request)) {
+            throw requestError("Empty request body", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+        }
 
         // decode and store the attributes that could exist in our
         // request body
 
-        AccessTokenBody accessTokenBody;
+        AccessTokenRequest accessTokenRequest;
         try {
-            accessTokenBody = new AccessTokenBody(request);
+            accessTokenRequest = new AccessTokenRequest(request, dataStore, ztsOAuthIssuer);
+            if (principal == null) {
+                ((RsrcCtxWrapper) ctx).setPrincipal(accessTokenRequest.getPrincipal());
+                principal = ((RsrcCtxWrapper) ctx).principal();
+                principalDomain = logPrincipalAndGetDomain(ctx);
+            }
         } catch (IllegalArgumentException ex) {
             throw requestError(ex.getMessage(), caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
         }
 
-        validateProxyForPrincipalValue(accessTokenBody.getProxyForPrincipal(), principalName,
+        // we want to log the request body in our access log so
+        // we know what is the client asking for, but we'll just
+        // limit the request up to 1K
+
+        ctx.request().setAttribute(ACCESS_LOG_ADDL_QUERY, accessTokenRequest.getQueryLogData());
+
+        // if our principal is still null, then our request was not properly
+        // authenticated so we'll return as such. otherwise, get our principal's name
+
+        if (principal == null) {
+            throw authError("Unauthenticated request", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
+        }
+        String principalName = principal.getFullName();
+
+        // if we have a proxy for principal value then we need to validate
+        // that the principal is authorized for such operations
+
+        validateProxyForPrincipalValue(accessTokenRequest.getProxyForPrincipal(), principalName,
                 principalDomain, caller);
 
         if (LOGGER.isDebugEnabled()) {
             LOGGER.debug("postAccessTokenRequest(principal: {}, grant-type: {}, scope: {}, expires-in: {}, proxy-for-principal: {})",
-                    principalName, accessTokenBody.getGrantType(), accessTokenBody.getScope(),
-                    accessTokenBody.getExpiryTime(), accessTokenBody.getProxyForPrincipal());
+                    principalName, accessTokenRequest.getGrantType(), accessTokenRequest.getScope(),
+                    accessTokenRequest.getExpiryTime(), accessTokenRequest.getProxyForPrincipal());
         }
 
         // our scopes are space separated list of values
 
-        AccessTokenRequest tokenRequest = new AccessTokenRequest(accessTokenBody.getScope());
+        AccessTokenScope tokenScope = new AccessTokenScope(accessTokenRequest.getScope());
 
         // before using any of our values let's validate that they
         // match our schema
 
-        final String domainName = tokenRequest.getDomainName();
+        final String domainName = tokenScope.getDomainName();
         setRequestDomain(ctx, domainName);
         validate(domainName, TYPE_DOMAIN_NAME, principalDomain, caller);
 
-        String[] requestedRoles = tokenRequest.getRoleNames(domainName);
+        String[] requestedRoles = tokenScope.getRoleNames(domainName);
         if (requestedRoles != null) {
             for (String requestedRole : requestedRoles) {
                 validate(requestedRole, TYPE_ENTITY_NAME, principalDomain, caller);
@@ -2463,7 +2536,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // to make sure the requested fields are valid according
         // to our configured authorization details entity for the role
 
-        validateAuthorizationDetails(accessTokenBody.getAuthzDetails(), requestedRoles, data, caller,
+        validateAuthorizationDetails(accessTokenRequest.getAuthzDetails(), requestedRoles, data, caller,
                 domainName, principalDomain);
 
         // check if the authorized service domain matches to the
@@ -2488,12 +2561,12 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // remove any roles that are authorized by only one of the principals
 
         String proxyUser = null;
-        if (accessTokenBody.getProxyForPrincipal() != null) {
+        if (accessTokenRequest.getProxyForPrincipal() != null) {
 
             // we also need to verify that we are not returning id tokens.
             // proxy principal functionality is only valid for access tokens
 
-            if (tokenRequest.isOpenIdScope()) {
+            if (tokenScope.isOpenIdScope()) {
                 throw requestError("Proxy Principal cannot request id tokens", caller,
                         domainName, principalDomain);
             }
@@ -2501,7 +2574,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
             // process the role lookup for the proxy principal
 
             Set<String> rolesForProxy = new HashSet<>();
-            dataStore.getAccessibleRoles(data, domainName, accessTokenBody.getProxyForPrincipal(), requestedRoles,
+            dataStore.getAccessibleRoles(data, domainName, accessTokenRequest.getProxyForPrincipal(), requestedRoles,
                     false, rolesForProxy, false);
             roles.retainAll(rolesForProxy);
 
@@ -2509,14 +2582,14 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
             // with an empty set
 
             if (roles.isEmpty()) {
-                throw forbiddenError(tokenErrorMessage(caller, accessTokenBody.getProxyForPrincipal(), domainName,
+                throw forbiddenError(tokenErrorMessage(caller, accessTokenRequest.getProxyForPrincipal(), domainName,
                         requestedRoles), caller, domainName, principalDomain);
             }
 
             // we need to switch our principal and proxy for user
 
             proxyUser = principalName;
-            principalName = accessTokenBody.getProxyForPrincipal();
+            principalName = accessTokenRequest.getProxyForPrincipal();
         }
 
         // if the request was done by a role certificate we need to make sure
@@ -2527,7 +2600,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
                     caller, domainName, principalDomain);
         }
 
-        long tokenTimeout = determineTokenTimeout(data, roles, null, accessTokenBody.getExpiryTime());
+        long tokenTimeout = determineTokenTimeout(data, roles, null, accessTokenRequest.getExpiryTime());
         long iat = System.currentTimeMillis() / 1000;
 
         AccessToken accessToken = new AccessToken();
@@ -2540,10 +2613,10 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         accessToken.setExpiryTime(iat + tokenTimeout);
         accessToken.setUserId(principalName);
         accessToken.setSubject(principalName);
-        accessToken.setIssuer(accessTokenBody.isUseOpenIDIssuer() ? ztsOpenIDIssuer : ztsOAuthIssuer);
+        accessToken.setIssuer(accessTokenRequest.isUseOpenIDIssuer() ? ztsOpenIDIssuer : ztsOAuthIssuer);
         accessToken.setProxyPrincipal(proxyUser);
         accessToken.setScope(new ArrayList<>(roles));
-        accessToken.setAuthorizationDetails(accessTokenBody.getAuthzDetails());
+        accessToken.setAuthorizationDetails(accessTokenRequest.getAuthzDetails());
 
         // if we have a certificate used for mTLS authentication then
         // we're going to bind the certificate to the access token
@@ -2552,8 +2625,8 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         X509Certificate cert = principal.getX509Certificate();
         if (cert != null) {
             accessToken.setConfirmX509CertHash(cert);
-            if (accessTokenBody.getProxyPrincipalsSpiffeUris() != null) {
-                accessToken.setConfirmProxyPrincipalSpiffeUris(accessTokenBody.getProxyPrincipalsSpiffeUris());
+            if (accessTokenRequest.getProxyPrincipalsSpiffeUris() != null) {
+                accessToken.setConfirmProxyPrincipalSpiffeUris(accessTokenRequest.getProxyPrincipalsSpiffeUris());
             }
         }
 
@@ -2563,16 +2636,16 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // now let's check to see if we need to create openid token
 
         String idJwts = null;
-        if (tokenRequest.isOpenIdScope()) {
+        if (tokenScope.isOpenIdScope()) {
 
-            final String serviceName = tokenRequest.getServiceName();
+            final String serviceName = tokenScope.getServiceName();
             validate(serviceName, TYPE_SIMPLE_NAME, principalDomain, caller);
 
             IdToken idToken = new IdToken();
             idToken.setVersion(1);
-            idToken.setAudience(tokenRequest.getDomainName() + "." + serviceName);
+            idToken.setAudience(tokenScope.getDomainName() + "." + serviceName);
             idToken.setSubject(principalName);
-            idToken.setIssuer(accessTokenBody.isUseOpenIDIssuer() ? ztsOpenIDIssuer : ztsOAuthIssuer);
+            idToken.setIssuer(accessTokenRequest.isUseOpenIDIssuer() ? ztsOpenIDIssuer : ztsOAuthIssuer);
 
             // id tokens are only valid for up to 12 hours max
             // (value configured as a system property).
@@ -2593,13 +2666,13 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // does not match the returned list of roles then we need to return the updated
         // set of scopes
 
-        if (tokenRequest.sendScopeResponse() || requestedRoles != null && requestedRoles.length != roles.size()) {
+        if (tokenScope.sendScopeResponse() || requestedRoles != null && requestedRoles.length != roles.size()) {
             List<String> domainRoles = new ArrayList<>();
             for (String role : roles) {
-                domainRoles.add(domainName + AccessTokenRequest.OBJECT_ROLE + role);
+                domainRoles.add(domainName + AccessTokenScope.OBJECT_ROLE + role);
             }
-            if (tokenRequest.isOpenIdScope()) {
-                domainRoles.add(AccessTokenRequest.OBJECT_OPENID);
+            if (tokenScope.isOpenIdScope()) {
+                domainRoles.add(AccessTokenScope.OBJECT_OPENID);
             }
             response.setScope(String.join(" ", domainRoles));
         }
@@ -4722,7 +4795,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // retrieve the public key for the request for verification
 
         final String keyId = userRequest || refreshOperation ? req.getKeyId() : principal.getKeyId();
-        String publicKey = getPublicKey(domain, service, keyId);
+        String publicKey = dataStore.getPublicKey(domain, service, keyId);
         if (publicKey == null) {
             throw requestError("Unable to retrieve public key for " + fullServiceName +
                     " with key id: " + keyId, caller, domain, principalDomain);
@@ -4967,7 +5040,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
 
         final String scope = StringUtil.isEmpty(athenzScope) ?
                 "openid " + ResourceUtils.roleResourceName(domainName, athenzRoleName) : athenzScope;
-        IdTokenRequest tokenRequest = new IdTokenRequest(scope);
+        IdTokenScope tokenScope = new IdTokenScope(scope);
 
         // check if the authorized service domain matches to the
         // requested domain name
@@ -4983,7 +5056,7 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         // now let's process our requests and see if we need to extract
         // either groups or roles for our response
 
-        List<String> idTokenGroups = processIdTokenRoles(principalName, tokenRequest,
+        List<String> idTokenGroups = processIdTokenRoles(principalName, tokenScope,
                 clientIdDomain, fullArn, allScopePresent, principalDomain, caller);
 
         long iat = System.currentTimeMillis() / 1000;
@@ -5464,6 +5537,11 @@ public class ZTSImpl implements KeyStore, ZTSHandler {
         return new ResourceException(code, new ResourceError().code(code).message(msg));
     }
 
+    protected RuntimeException authError(final String msg, final String caller, final String requestDomain,
+                                            final String principalDomain) {
+        return error(ResourceException.UNAUTHORIZED, msg, caller, requestDomain, principalDomain);
+    }
+
     protected RuntimeException requestError(final String msg, final String caller, final String requestDomain,
                                             final String principalDomain) {
         return error(ResourceException.BAD_REQUEST, msg, caller, requestDomain, principalDomain);
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSResources.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSResources.java
index 307e9cf15..7127d22bc 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSResources.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/ZTSResources.java
@@ -967,6 +967,42 @@ public class ZTSResources {
         }
     }
 
+    @POST
+    @Path("/oauth2/introspect")
+    @Consumes("application/x-www-form-urlencoded")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Operation(description = "OAuth2 Access Token Introspection. The server will carry out the required authorization check of authorize (\"introspect\", \"{domain}:token\"); where domain is the audience of the token.")
+    public IntrospectResponse postIntrospectRequest(
+        @Parameter(description = "token introspect request details", required = true) String request) {
+        int code = ResourceException.OK;
+        ResourceContext context = null;
+        try {
+            context = this.delegate.newResourceContext(this.servletContext, this.request, this.response, "postIntrospectRequest");
+            context.authenticate();
+            return this.delegate.postIntrospectRequest(context, request);
+        } catch (ResourceException e) {
+            code = e.getCode();
+            switch (code) {
+            case ResourceException.BAD_REQUEST:
+                throw typedException(code, e, ResourceError.class);
+            case ResourceException.FORBIDDEN:
+                throw typedException(code, e, ResourceError.class);
+            case ResourceException.NOT_FOUND:
+                throw typedException(code, e, ResourceError.class);
+            case ResourceException.TOO_MANY_REQUESTS:
+                throw typedException(code, e, ResourceError.class);
+            case ResourceException.UNAUTHORIZED:
+                throw typedException(code, e, ResourceError.class);
+            default:
+                System.err.println("*** Warning: undeclared exception (" + code + ") for resource postIntrospectRequest");
+                throw typedException(code, e, ResourceError.class);
+            }
+        } finally {
+            this.delegate.publishChangeMessage(context, code);
+            this.delegate.recordMetrics(context, code);
+        }
+    }
+
     @POST
     @Path("/rolecert")
     @Consumes(MediaType.APPLICATION_JSON)
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java
index 25e943580..2e49be323 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/store/DataStore.java
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.yahoo.athenz.auth.AuthorityConsts;
+import com.yahoo.athenz.auth.KeyStore;
 import com.yahoo.athenz.auth.util.StringUtils;
 import com.yahoo.athenz.common.metrics.Metric;
 import com.yahoo.athenz.common.server.db.RolesProvider;
@@ -67,13 +68,14 @@ import static com.yahoo.athenz.common.ServerCommonConsts.ATHENZ_SYS_DOMAIN;
 import static com.yahoo.athenz.common.ServerCommonConsts.PROP_ATHENZ_CONF;
 import static com.yahoo.athenz.zts.ZTSConsts.ZTS_ISSUE_ROLE_CERT_TAG;
 
-public class DataStore implements DataCacheProvider, RolesProvider, PubKeysProvider {
+public class DataStore implements DataCacheProvider, RolesProvider, PubKeysProvider, KeyStore {
 
     ChangeLogStore changeLogStore;
     private CloudStore cloudStore;
     protected final Metric metric;
     private final Cache<String, DataCache> cacheStore;
     final Cache<String, PublicKey> zmsPublicKeyCache;
+    final Cache<String, PublicKey> svcPublicKeyCache;
     final Cache<String, List<GroupMember>> groupMemberCache;
     final Cache<String, List<GroupMember>> principalGroupCache;
     final RequireRoleCertCache requireRoleCertCache;
@@ -139,6 +141,7 @@ public class DataStore implements DataCacheProvider, RolesProvider, PubKeysProvi
 
         hostCache = new HashMap<>();
         publicKeyCache = new HashMap<>();
+        svcPublicKeyCache = CacheBuilder.newBuilder().concurrencyLevel(25).build();
 
         // our configured values are going to be in seconds, so we need
         // to convert our input in seconds to milliseconds
@@ -583,6 +586,15 @@ public class DataStore implements DataCacheProvider, RolesProvider, PubKeysProvi
     }
 
     String generateServiceKeyName(String domain, String service, String keyId) {
+        if (domain != null) {
+            domain = domain.toLowerCase();
+        }
+        if (service != null) {
+            service = service.toLowerCase();
+        }
+        if (keyId != null) {
+            keyId = keyId.toLowerCase();
+        }
         return domain + "." + service + "_" + keyId;
     }
 
@@ -1443,6 +1455,7 @@ boolean loadZtsJwk(ArrayList<com.yahoo.athenz.zms.PublicKeyEntry> keys) {
 
         for (Map.Entry<String, String> entry : publicKeyMap.entrySet()) {
             publicKeyCache.remove(entry.getKey());
+            svcPublicKeyCache.invalidate(entry.getKey());
         }
     }
 
@@ -1892,12 +1905,41 @@ boolean loadZtsJwk(ArrayList<com.yahoo.athenz.zms.PublicKeyEntry> keys) {
                 requestedRoleList, accessibleRoles, keepFullName);
     }
 
-    // API
-    public String getPublicKey(String domain, String service, String keyId) {
+    public PublicKey getServicePublicKey(final String domain, final String service, final String keyId) {
 
-        String publicKeyName = generateServiceKeyName(domain, service, keyId);
-        String publicKey;
+        // if our public key is in our cache then we'll just return it right away
+
+        final String publicKeyName = generateServiceKeyName(domain, service, keyId);
+        PublicKey publicKey = svcPublicKeyCache.getIfPresent(publicKeyName);
+        if (publicKey != null) {
+            return publicKey;
+        }
+
+        // extract the public key from the pem cache
+
+        final String pemKey = getPemPublicKey(publicKeyName);
+        if (pemKey == null) {
+            return null;
+        }
+
+        // convert the pem key to public key object
+
+        try {
+            publicKey = Crypto.loadPublicKey(pemKey);
+            if (publicKey != null) {
+                svcPublicKeyCache.put(publicKeyName, publicKey);
+            }
+            return publicKey;
+        } catch (Exception ex) {
+            LOGGER.error("Unable to load public key: {} for service: {} in domain: {}",
+                    keyId, service, domain);
+            return null;
+        }
+    }
+
+    public String getPemPublicKey(final String publicKeyName) {
 
+        String publicKey;
         try {
             pkeyRLock.lock();
             publicKey = publicKeyCache.get(publicKeyName);
@@ -1905,6 +1947,15 @@ boolean loadZtsJwk(ArrayList<com.yahoo.athenz.zms.PublicKeyEntry> keys) {
             pkeyRLock.unlock();
         }
 
+        return publicKey;
+    }
+
+    // API
+    public String getPublicKey(String domain, String service, String keyId) {
+
+        final String publicKeyName = generateServiceKeyName(domain, service, keyId);
+        final String publicKey = getPemPublicKey(publicKeyName);
+
         if (publicKey == null && LOGGER.isDebugEnabled()) {
             LOGGER.debug("Public key: {} not available", publicKeyName);
         }
@@ -1988,7 +2039,6 @@ boolean loadZtsJwk(ArrayList<com.yahoo.athenz.zms.PublicKeyEntry> keys) {
         return requireRoleCertCache.getRolesRequireRoleCert(principal);
     }
 
-
     class DataUpdater implements Runnable {
 
         @Override
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenBody.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenBody.java
deleted file mode 100644
index 3d294848a..000000000
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenBody.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright The Athenz Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.yahoo.athenz.zts.token;
-
-import com.yahoo.athenz.zts.ZTSConsts;
-import com.yahoo.athenz.zts.utils.ZTSUtils;
-import org.eclipse.jetty.util.StringUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class AccessTokenBody {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenBody.class);
-
-    private static final String KEY_SCOPE = "scope";
-    private static final String KEY_GRANT_TYPE = "grant_type";
-    private static final String KEY_EXPIRES_IN = "expires_in";
-    private static final String KEY_PROXY_FOR_PRINCIPAL = "proxy_for_principal";
-    private static final String KEY_AUTHORIZATION_DETAILS = "authorization_details";
-    private static final String KEY_PROXY_PRINCIPAL_SPIFFE_URIS = "proxy_principal_spiffe_uris";
-    private static final String KEY_OPENID_ISSUER = "openid_issuer";
-    private static final String OAUTH_GRANT_CREDENTIALS = "client_credentials";
-
-    String grantType = null;
-    String scope = null;
-    String proxyForPrincipal = null;
-    String authzDetails = null;
-    List<String> proxyPrincipalsSpiffeUris = null;
-    int expiryTime = 0;
-    boolean useOpenIDIssuer = false;
-
-    public AccessTokenBody(String body) {
-
-        String[] comps = body.split("&");
-        for (String comp : comps) {
-            int idx = comp.indexOf('=');
-            if (idx == -1) {
-                if (LOGGER.isDebugEnabled()) {
-                    LOGGER.debug("AccessTokenBody: skipping invalid component (missing separator): {}", comp);
-                }
-                continue;
-            }
-            final String key = decodeString(comp.substring(0, idx));
-            if (key == null) {
-                if (LOGGER.isDebugEnabled()) {
-                    LOGGER.debug("AccessTokenBody: skipping invalid component (null key): {}", comp);
-                }
-                continue;
-            }
-            final String value = decodeString(comp.substring(idx + 1));
-            if (value == null) {
-                continue;
-            }
-            switch (key) {
-                case KEY_GRANT_TYPE:
-                    grantType = value.toLowerCase();
-                    break;
-                case KEY_SCOPE:
-                    scope = value.toLowerCase();
-                    break;
-                case KEY_EXPIRES_IN:
-                    expiryTime = ZTSUtils.parseInt(value, 0);
-                    break;
-                case KEY_PROXY_FOR_PRINCIPAL:
-                    proxyForPrincipal = value.toLowerCase();
-                    break;
-                case KEY_AUTHORIZATION_DETAILS:
-                    authzDetails = value;
-                    break;
-                case KEY_PROXY_PRINCIPAL_SPIFFE_URIS:
-                    proxyPrincipalsSpiffeUris = getProxyPrincipalSpiffeUris(value.toLowerCase());
-                    break;
-                case KEY_OPENID_ISSUER:
-                    useOpenIDIssuer = Boolean.parseBoolean(value);
-                    break;
-            }
-        }
-
-        // validate the request data
-
-        if (!OAUTH_GRANT_CREDENTIALS.equals(grantType)) {
-            throw new IllegalArgumentException("Invalid grant request: " + grantType);
-        }
-
-        // we must have scope provided so we know what access
-        // the client is looking for
-
-        if (StringUtil.isEmpty(scope)) {
-            throw new IllegalArgumentException("Invalid request: no scope provided");
-        }
-    }
-
-    public String getGrantType() {
-        return grantType;
-    }
-
-    public String getScope() {
-        return scope;
-    }
-
-    public String getProxyForPrincipal() {
-        return proxyForPrincipal;
-    }
-
-    public String getAuthzDetails() {
-        return authzDetails;
-    }
-
-    public List<String> getProxyPrincipalsSpiffeUris() {
-        return proxyPrincipalsSpiffeUris;
-    }
-
-    public int getExpiryTime() {
-        return expiryTime;
-    }
-
-    public boolean isUseOpenIDIssuer() {
-        return useOpenIDIssuer;
-    }
-
-    String decodeString(final String encodedString) {
-        try {
-            return URLDecoder.decode(encodedString, StandardCharsets.UTF_8);
-        } catch (Exception ex) {
-            LOGGER.error("Unable to decode: {}, error: {}", encodedString, ex.getMessage());
-            return null;
-        }
-    }
-
-    List<String> getProxyPrincipalSpiffeUris(final String proxyPrincipalSpiffeUris) {
-
-        if (proxyPrincipalSpiffeUris.isEmpty()) {
-            return null;
-        }
-
-        List<String> uris = Stream.of(proxyPrincipalSpiffeUris.split(","))
-                .map(String::trim)
-                .collect(Collectors.toList());
-
-        // verify that all values are valid spiffe uris structurally
-
-        for (String uri : uris) {
-            if (!uri.startsWith(ZTSConsts.ZTS_CERT_SPIFFE_URI)) {
-                throw new IllegalArgumentException("Invalid spiffe uri specified: " + uri);
-            }
-
-            try {
-                new URI(uri);
-            } catch (URISyntaxException ex) {
-                throw new IllegalArgumentException("Invalid spiffe uri specified: " + uri);
-            }
-        }
-
-        return uris;
-    }
-}
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java
index 183bf8152..e361157e3 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenRequest.java
@@ -15,45 +15,236 @@
  */
 package com.yahoo.athenz.zts.token;
 
+import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.Principal;
+import com.yahoo.athenz.auth.impl.SimplePrincipal;
+import com.yahoo.athenz.auth.token.OAuth2Token;
 import com.yahoo.athenz.zts.ZTSConsts;
+import com.yahoo.athenz.zts.utils.ZTSUtils;
 import org.eclipse.jetty.util.StringUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-public class AccessTokenRequest extends OAuthTokenRequest {
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
-    private static boolean supportOpenIdScope = Boolean.parseBoolean(
-            System.getProperty(ZTSConsts.ZTS_PROP_OAUTH_OPENID_SCOPE, "false"));
+public class AccessTokenRequest {
 
-    public AccessTokenRequest(final String scope) {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenRequest.class);
 
-        // the format of our scopes for role access token and id tokens are:
-        // access token/id token combo:
-        //   <domainName>:domain
-        //   <domainName>:role.<roleName>
-        //   openid <domainName>:service.<serviceName>
+    private static final String KEY_SCOPE = "scope";
+    private static final String KEY_GRANT_TYPE = "grant_type";
+    private static final String KEY_EXPIRES_IN = "expires_in";
+    private static final String KEY_PROXY_FOR_PRINCIPAL = "proxy_for_principal";
+    private static final String KEY_AUTHORIZATION_DETAILS = "authorization_details";
+    private static final String KEY_PROXY_PRINCIPAL_SPIFFE_URIS = "proxy_principal_spiffe_uris";
+    private static final String KEY_OPENID_ISSUER = "openid_issuer";
+    private static final String KEY_CLIENT_ASSERTION = "client_assertion";
+    private static final String KEY_CLIENT_ASSERTION_TYPE = "client_assertion_type";
 
-        super(scope, 1, null);
+    private static final String OAUTH_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
+    private static final String OAUTH_GRANT_CREDENTIALS = "client_credentials";
 
-        // if we don't have a domain then it's invalid scope
+    String grantType = null;
+    String scope = null;
+    String proxyForPrincipal = null;
+    String authzDetails = null;
+    String clientAssertion = null;
+    String clientAssertionType = null;
+    List<String> proxyPrincipalsSpiffeUris = null;
+    Principal principal = null;
+    int expiryTime = 0;
+    boolean useOpenIDIssuer = false;
 
-        if (StringUtil.isEmpty(getDomainName())) {
-            throw error("No domains in scope", scope);
+    public AccessTokenRequest(final String body, KeyStore publicKeyProvider, final String oauth2Issuer) {
+
+        String[] comps = body.split("&");
+        for (String comp : comps) {
+            int idx = comp.indexOf('=');
+            if (idx == -1) {
+                if (LOGGER.isDebugEnabled()) {
+                    LOGGER.debug("AccessTokenBody: skipping invalid component (missing separator): {}", comp);
+                }
+                continue;
+            }
+            final String key = decodeString(comp.substring(0, idx));
+            if (key == null) {
+                if (LOGGER.isDebugEnabled()) {
+                    LOGGER.debug("AccessTokenBody: skipping invalid component (null key): {}", comp);
+                }
+                continue;
+            }
+            final String value = decodeString(comp.substring(idx + 1));
+            if (value == null) {
+                continue;
+            }
+            switch (key) {
+                case KEY_GRANT_TYPE:
+                    grantType = value.toLowerCase();
+                    break;
+                case KEY_SCOPE:
+                    scope = value.toLowerCase();
+                    break;
+                case KEY_EXPIRES_IN:
+                    expiryTime = ZTSUtils.parseInt(value, 0);
+                    break;
+                case KEY_PROXY_FOR_PRINCIPAL:
+                    proxyForPrincipal = value.toLowerCase();
+                    break;
+                case KEY_AUTHORIZATION_DETAILS:
+                    authzDetails = value;
+                    break;
+                case KEY_PROXY_PRINCIPAL_SPIFFE_URIS:
+                    proxyPrincipalsSpiffeUris = getProxyPrincipalSpiffeUris(value.toLowerCase());
+                    break;
+                case KEY_OPENID_ISSUER:
+                    useOpenIDIssuer = Boolean.parseBoolean(value);
+                    break;
+                case KEY_CLIENT_ASSERTION_TYPE:
+                    clientAssertionType = value.toLowerCase();
+                    break;
+                case KEY_CLIENT_ASSERTION:
+                    clientAssertion = value;
+                    break;
+            }
+        }
+
+        // validate the request data
+
+        if (!OAUTH_GRANT_CREDENTIALS.equals(grantType)) {
+            throw new IllegalArgumentException("Invalid grant request: " + grantType);
+        }
+
+        // we must have scope provided so we know what access
+        // the client is looking for
+
+        if (StringUtil.isEmpty(scope)) {
+            throw new IllegalArgumentException("Invalid request: no scope provided");
+        }
+
+        // if we're provided with a client assertion then we must
+        // have a client assertion type as well
+
+        if (!StringUtil.isEmpty(clientAssertion)) {
+
+            if (StringUtil.isEmpty(clientAssertionType)) {
+                throw new IllegalArgumentException("Invalid request: no client assertion type provided");
+            } else if (!OAUTH_JWT_BEARER.equals(clientAssertionType)) {
+                throw new IllegalArgumentException("Invalid client assertion type: " + clientAssertionType);
+            }
+
+            // now let's check if we have a valid client assertion
+            // token provided and, if yes, generate our principal object
+
+            try {
+                OAuth2Token token = new OAuth2Token(clientAssertion, publicKeyProvider, oauth2Issuer);
+                principal = SimplePrincipal.create(token.getClientIdDomainName(),
+                        token.getClientIdServiceName(), clientAssertion, token.getIssueTime(), null);
+            } catch (Exception ex) {
+                throw new IllegalArgumentException("Invalid client assertion: " + ex.getMessage());
+            }
         }
+    }
+
+    public String getGrantType() {
+        return grantType;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public String getProxyForPrincipal() {
+        return proxyForPrincipal;
+    }
+
+    public String getAuthzDetails() {
+        return authzDetails;
+    }
 
-        // for openid scope we must have the openid scope
-        // along with the service name since the audience
-        // must be set for that service only
+    public List<String> getProxyPrincipalsSpiffeUris() {
+        return proxyPrincipalsSpiffeUris;
+    }
+
+    public int getExpiryTime() {
+        return expiryTime;
+    }
 
-        if (openIdScope && StringUtil.isEmpty(serviceName)) {
-            throw error("No audience service name for openid scope", scope);
+    public boolean isUseOpenIDIssuer() {
+        return useOpenIDIssuer;
+    }
+
+    public Principal getPrincipal() {
+        return principal;
+    }
+
+    public String getQueryLogData() {
+
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("scope=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8));
+        if (expiryTime > 0) {
+            stringBuilder.append("&expires_in=").append(expiryTime);
+        }
+        if (!StringUtil.isEmpty(proxyForPrincipal)) {
+            stringBuilder.append("&proxy_for_principal=").append(URLEncoder.encode(proxyForPrincipal, StandardCharsets.UTF_8));
+        }
+        if (!StringUtil.isEmpty(authzDetails)) {
+            stringBuilder.append("&authorization_details=").append(URLEncoder.encode(authzDetails, StandardCharsets.UTF_8));
+        }
+        if (proxyPrincipalsSpiffeUris != null && !proxyPrincipalsSpiffeUris.isEmpty()) {
+            stringBuilder.append("&proxy_principal_spiffe_uris=");
+            for (String uri : proxyPrincipalsSpiffeUris) {
+                stringBuilder.append(URLEncoder.encode(uri, StandardCharsets.UTF_8)).append(',');
+            }
+            stringBuilder.setLength(stringBuilder.length() - 1);
         }
+
+        // make sure our log line is limited to 1024 characters
+        // the data is already url encoded
+
+        stringBuilder.setLength(Math.min(stringBuilder.length(), 1024));
+        return stringBuilder.toString();
     }
 
-    @Override
-    public boolean isOpenIdScope() {
-        return supportOpenIdScope && openIdScope;
+    String decodeString(final String encodedString) {
+        try {
+            return URLDecoder.decode(encodedString, StandardCharsets.UTF_8);
+        } catch (Exception ex) {
+            LOGGER.error("Unable to decode: {}, error: {}", encodedString, ex.getMessage());
+            return null;
+        }
     }
 
-    public static void setSupportOpenIdScope(boolean value) {
-        supportOpenIdScope = value;
+    List<String> getProxyPrincipalSpiffeUris(final String proxyPrincipalSpiffeUris) {
+
+        if (proxyPrincipalSpiffeUris.isEmpty()) {
+            return null;
+        }
+
+        List<String> uris = Stream.of(proxyPrincipalSpiffeUris.split(","))
+                .map(String::trim)
+                .collect(Collectors.toList());
+
+        // verify that all values are valid spiffe uris structurally
+
+        for (String uri : uris) {
+            if (!uri.startsWith(ZTSConsts.ZTS_CERT_SPIFFE_URI)) {
+                throw new IllegalArgumentException("Invalid spiffe uri specified: " + uri);
+            }
+
+            try {
+                new URI(uri);
+            } catch (URISyntaxException ex) {
+                throw new IllegalArgumentException("Invalid spiffe uri specified: " + uri);
+            }
+        }
+
+        return uris;
     }
 }
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenScope.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenScope.java
new file mode 100644
index 000000000..e560c88c0
--- /dev/null
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/AccessTokenScope.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yahoo.athenz.zts.token;
+
+import com.yahoo.athenz.zts.ZTSConsts;
+import org.eclipse.jetty.util.StringUtil;
+
+public class AccessTokenScope extends OAuthTokenScope {
+
+    private static boolean supportOpenIdScope = Boolean.parseBoolean(
+            System.getProperty(ZTSConsts.ZTS_PROP_OAUTH_OPENID_SCOPE, "false"));
+
+    public AccessTokenScope(final String scope) {
+
+        // the format of our scopes for role access token and id tokens are:
+        // access token/id token combo:
+        //   <domainName>:domain
+        //   <domainName>:role.<roleName>
+        //   openid <domainName>:service.<serviceName>
+
+        super(scope, 1, null);
+
+        // if we don't have a domain then it's invalid scope
+
+        if (StringUtil.isEmpty(getDomainName())) {
+            throw error("No domains in scope", scope);
+        }
+
+        // for openid scope we must have the openid scope
+        // along with the service name since the audience
+        // must be set for that service only
+
+        if (openIdScope && StringUtil.isEmpty(serviceName)) {
+            throw error("No audience service name for openid scope", scope);
+        }
+    }
+
+    @Override
+    public boolean isOpenIdScope() {
+        return supportOpenIdScope && openIdScope;
+    }
+
+    public static void setSupportOpenIdScope(boolean value) {
+        supportOpenIdScope = value;
+    }
+}
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenRequest.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenScope.java
similarity index 95%
rename from servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenRequest.java
rename to servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenScope.java
index ae087fad5..06d2fa00a 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenRequest.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/IdTokenScope.java
@@ -20,7 +20,7 @@ import com.yahoo.athenz.zts.ZTSConsts;
 
 import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER;
 
-public class IdTokenRequest extends OAuthTokenRequest {
+public class IdTokenScope extends OAuthTokenScope {
 
     private static int maxDomains = Integer.parseInt(
             System.getProperty(ZTSConsts.ZTS_PROP_ID_TOKEN_MAX_DOMAINS, "10"));
@@ -32,7 +32,7 @@ public class IdTokenRequest extends OAuthTokenRequest {
         maxDomains = numDomains;
     }
 
-    public IdTokenRequest(final String scope) {
+    public IdTokenScope(final String scope) {
 
         // id token requests (service is required uri client_id parameter):
         //   openid
diff --git a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenRequest.java b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenScope.java
similarity index 98%
rename from servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenRequest.java
rename to servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenScope.java
index 47c02e4ac..67415411d 100644
--- a/servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenRequest.java
+++ b/servers/zts/src/main/java/com/yahoo/athenz/zts/token/OAuthTokenScope.java
@@ -23,9 +23,9 @@ import org.slf4j.LoggerFactory;
 
 import java.util.*;
 
-public class OAuthTokenRequest {
+public class OAuthTokenScope {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(OAuthTokenRequest.class);
+    private static final Logger LOGGER = LoggerFactory.getLogger(OAuthTokenScope.class);
 
     public static String OBJECT_DOMAIN  = ":domain";
     public static String OBJECT_ROLE    = ":role.";
@@ -45,7 +45,7 @@ public class OAuthTokenRequest {
     boolean rolesScope = false;
     int maxDomains;
 
-    public OAuthTokenRequest(final String scope, int maxDomains, DynamicConfigCsv systemAllowedRoles) {
+    public OAuthTokenScope(final String scope, int maxDomains, DynamicConfigCsv systemAllowedRoles) {
 
         this.maxDomains = maxDomains;
         if (this.maxDomains < 1) {
diff --git a/syncers/auth_history_syncer/pom.xml b/syncers/auth_history_syncer/pom.xml
index 6a4d5695c..55877ec54 100644
--- a/syncers/auth_history_syncer/pom.xml
+++ b/syncers/auth_history_syncer/pom.xml
@@ -23,7 +23,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/syncers/zms_aws_domain_syncer/pom.xml b/syncers/zms_aws_domain_syncer/pom.xml
index 4a05072cf..e68aec668 100644
--- a/syncers/zms_aws_domain_syncer/pom.xml
+++ b/syncers/zms_aws_domain_syncer/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 9dcc7e8f3..0a8767982 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -25,7 +25,7 @@
                 "axios": "1.7.8",
                 "body-parser": "1.20.3",
                 "cookie": "0.7.0",
-                "cookie-parser": "1.4.6",
+                "cookie-parser": "1.4.7",
                 "cookie-session": "2.0.0",
                 "csurf": "1.11.0",
                 "cytoscape": "3.22.1",
@@ -103,7 +103,6 @@
                 "react-test-renderer": "18.2.0",
                 "sinon": "14.0.0",
                 "supertest": "7.0.0",
-                "webdriver-manager": "12.1.9",
                 "webdriverio": "9.4.1"
             }
         },
@@ -889,9 +888,9 @@
             "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
         },
         "node_modules/@esbuild/aix-ppc64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
-            "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
+            "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
             "cpu": [
                 "ppc64"
             ],
@@ -906,9 +905,9 @@
             }
         },
         "node_modules/@esbuild/android-arm": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
-            "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
+            "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
             "cpu": [
                 "arm"
             ],
@@ -923,9 +922,9 @@
             }
         },
         "node_modules/@esbuild/android-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
-            "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
+            "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
             "cpu": [
                 "arm64"
             ],
@@ -940,9 +939,9 @@
             }
         },
         "node_modules/@esbuild/android-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
-            "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
+            "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
             "cpu": [
                 "x64"
             ],
@@ -957,9 +956,9 @@
             }
         },
         "node_modules/@esbuild/darwin-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
-            "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
+            "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
             "cpu": [
                 "arm64"
             ],
@@ -974,9 +973,9 @@
             }
         },
         "node_modules/@esbuild/darwin-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
-            "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
+            "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
             "cpu": [
                 "x64"
             ],
@@ -991,9 +990,9 @@
             }
         },
         "node_modules/@esbuild/freebsd-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
-            "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
             "cpu": [
                 "arm64"
             ],
@@ -1008,9 +1007,9 @@
             }
         },
         "node_modules/@esbuild/freebsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
-            "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
+            "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
             "cpu": [
                 "x64"
             ],
@@ -1025,9 +1024,9 @@
             }
         },
         "node_modules/@esbuild/linux-arm": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
-            "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
+            "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
             "cpu": [
                 "arm"
             ],
@@ -1042,9 +1041,9 @@
             }
         },
         "node_modules/@esbuild/linux-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
-            "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
+            "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
             "cpu": [
                 "arm64"
             ],
@@ -1059,9 +1058,9 @@
             }
         },
         "node_modules/@esbuild/linux-ia32": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
-            "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
+            "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
             "cpu": [
                 "ia32"
             ],
@@ -1076,9 +1075,9 @@
             }
         },
         "node_modules/@esbuild/linux-loong64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
-            "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
+            "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
             "cpu": [
                 "loong64"
             ],
@@ -1093,9 +1092,9 @@
             }
         },
         "node_modules/@esbuild/linux-mips64el": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
-            "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
+            "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
             "cpu": [
                 "mips64el"
             ],
@@ -1110,9 +1109,9 @@
             }
         },
         "node_modules/@esbuild/linux-ppc64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
-            "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
+            "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
             "cpu": [
                 "ppc64"
             ],
@@ -1127,9 +1126,9 @@
             }
         },
         "node_modules/@esbuild/linux-riscv64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
-            "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
+            "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
             "cpu": [
                 "riscv64"
             ],
@@ -1144,9 +1143,9 @@
             }
         },
         "node_modules/@esbuild/linux-s390x": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
-            "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
+            "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
             "cpu": [
                 "s390x"
             ],
@@ -1161,9 +1160,9 @@
             }
         },
         "node_modules/@esbuild/linux-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
-            "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
+            "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
             "cpu": [
                 "x64"
             ],
@@ -1177,10 +1176,27 @@
                 "node": ">=18"
             }
         },
+        "node_modules/@esbuild/netbsd-arm64": {
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "netbsd"
+            ],
+            "engines": {
+                "node": ">=18"
+            }
+        },
         "node_modules/@esbuild/netbsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
-            "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
+            "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
             "cpu": [
                 "x64"
             ],
@@ -1195,9 +1211,9 @@
             }
         },
         "node_modules/@esbuild/openbsd-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
-            "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
             "cpu": [
                 "arm64"
             ],
@@ -1212,9 +1228,9 @@
             }
         },
         "node_modules/@esbuild/openbsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
-            "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
+            "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
             "cpu": [
                 "x64"
             ],
@@ -1229,9 +1245,9 @@
             }
         },
         "node_modules/@esbuild/sunos-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
-            "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
+            "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
             "cpu": [
                 "x64"
             ],
@@ -1246,9 +1262,9 @@
             }
         },
         "node_modules/@esbuild/win32-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
-            "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
+            "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
             "cpu": [
                 "arm64"
             ],
@@ -1263,9 +1279,9 @@
             }
         },
         "node_modules/@esbuild/win32-ia32": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
-            "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
+            "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
             "cpu": [
                 "ia32"
             ],
@@ -1280,9 +1296,9 @@
             }
         },
         "node_modules/@esbuild/win32-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
-            "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
+            "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
             "cpu": [
                 "x64"
             ],
@@ -4341,15 +4357,6 @@
                 "node": ">=0.4.0"
             }
         },
-        "node_modules/adm-zip": {
-            "version": "0.5.14",
-            "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.14.tgz",
-            "integrity": "sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg==",
-            "dev": true,
-            "engines": {
-                "node": ">=12.0"
-            }
-        },
         "node_modules/agent-base": {
             "version": "7.1.1",
             "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
@@ -4362,22 +4369,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/ajv": {
-            "version": "6.12.6",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-            "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
         "node_modules/ansi-colors": {
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -4714,60 +4705,12 @@
             "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
             "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
         },
-        "node_modules/array-union": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
-            "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
-            "dev": true,
-            "dependencies": {
-                "array-uniq": "^1.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/array-uniq": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
-            "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/arrify": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
-            "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/asap": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
             "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
             "dev": true
         },
-        "node_modules/asn1": {
-            "version": "0.2.6",
-            "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-            "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-            "dev": true,
-            "dependencies": {
-                "safer-buffer": "~2.1.0"
-            }
-        },
-        "node_modules/assert-plus": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-            "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.8"
-            }
-        },
         "node_modules/ast-types": {
             "version": "0.13.4",
             "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -4842,21 +4785,6 @@
                 "uuid": "dist/bin/uuid"
             }
         },
-        "node_modules/aws-sign2": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-            "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-            "dev": true,
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/aws4": {
-            "version": "1.13.0",
-            "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz",
-            "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
-            "dev": true
-        },
         "node_modules/axios": {
             "version": "1.7.8",
             "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
@@ -5117,15 +5045,6 @@
                 "node": ">=10.0.0"
             }
         },
-        "node_modules/bcrypt-pbkdf": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-            "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-            "dev": true,
-            "dependencies": {
-                "tweetnacl": "^0.14.3"
-            }
-        },
         "node_modules/binary-extensions": {
             "version": "2.3.0",
             "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -5435,12 +5354,6 @@
                 "upper-case-first": "^2.0.2"
             }
         },
-        "node_modules/caseless": {
-            "version": "0.12.0",
-            "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-            "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-            "dev": true
-        },
         "node_modules/chalk": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -6067,11 +5980,12 @@
             }
         },
         "node_modules/cookie-parser": {
-            "version": "1.4.6",
-            "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
-            "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+            "version": "1.4.7",
+            "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+            "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+            "license": "MIT",
             "dependencies": {
-                "cookie": "0.4.1",
+                "cookie": "0.7.2",
                 "cookie-signature": "1.0.6"
             },
             "engines": {
@@ -6079,9 +5993,9 @@
             }
         },
         "node_modules/cookie-parser/node_modules/cookie": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-            "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+            "version": "0.7.2",
+            "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+            "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
             "license": "MIT",
             "engines": {
                 "node": ">= 0.6"
@@ -6678,18 +6592,6 @@
                 "lodash": "^4.17.15"
             }
         },
-        "node_modules/dashdash": {
-            "version": "1.14.1",
-            "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-            "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-            "dev": true,
-            "dependencies": {
-                "assert-plus": "^1.0.0"
-            },
-            "engines": {
-                "node": ">=0.10"
-            }
-        },
         "node_modules/data-uri-to-buffer": {
             "version": "4.0.1",
             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -6927,24 +6829,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/del": {
-            "version": "2.2.2",
-            "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
-            "integrity": "sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ==",
-            "dev": true,
-            "dependencies": {
-                "globby": "^5.0.0",
-                "is-path-cwd": "^1.0.0",
-                "is-path-in-cwd": "^1.0.0",
-                "object-assign": "^4.0.1",
-                "pify": "^2.0.0",
-                "pinkie-promise": "^2.0.0",
-                "rimraf": "^2.2.8"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/delayed-stream": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -7190,22 +7074,6 @@
                 "wcwidth": "^1.0.1"
             }
         },
-        "node_modules/ecc-jsbn": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-            "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-            "dev": true,
-            "dependencies": {
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.1.0"
-            }
-        },
-        "node_modules/ecc-jsbn/node_modules/jsbn": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-            "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-            "dev": true
-        },
         "node_modules/edge-paths": {
             "version": "3.0.5",
             "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz",
@@ -7493,9 +7361,9 @@
             }
         },
         "node_modules/esbuild": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
-            "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
+            "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
             "dev": true,
             "hasInstallScript": true,
             "license": "MIT",
@@ -7506,30 +7374,31 @@
                 "node": ">=18"
             },
             "optionalDependencies": {
-                "@esbuild/aix-ppc64": "0.23.1",
-                "@esbuild/android-arm": "0.23.1",
-                "@esbuild/android-arm64": "0.23.1",
-                "@esbuild/android-x64": "0.23.1",
-                "@esbuild/darwin-arm64": "0.23.1",
-                "@esbuild/darwin-x64": "0.23.1",
-                "@esbuild/freebsd-arm64": "0.23.1",
-                "@esbuild/freebsd-x64": "0.23.1",
-                "@esbuild/linux-arm": "0.23.1",
-                "@esbuild/linux-arm64": "0.23.1",
-                "@esbuild/linux-ia32": "0.23.1",
-                "@esbuild/linux-loong64": "0.23.1",
-                "@esbuild/linux-mips64el": "0.23.1",
-                "@esbuild/linux-ppc64": "0.23.1",
-                "@esbuild/linux-riscv64": "0.23.1",
-                "@esbuild/linux-s390x": "0.23.1",
-                "@esbuild/linux-x64": "0.23.1",
-                "@esbuild/netbsd-x64": "0.23.1",
-                "@esbuild/openbsd-arm64": "0.23.1",
-                "@esbuild/openbsd-x64": "0.23.1",
-                "@esbuild/sunos-x64": "0.23.1",
-                "@esbuild/win32-arm64": "0.23.1",
-                "@esbuild/win32-ia32": "0.23.1",
-                "@esbuild/win32-x64": "0.23.1"
+                "@esbuild/aix-ppc64": "0.25.0",
+                "@esbuild/android-arm": "0.25.0",
+                "@esbuild/android-arm64": "0.25.0",
+                "@esbuild/android-x64": "0.25.0",
+                "@esbuild/darwin-arm64": "0.25.0",
+                "@esbuild/darwin-x64": "0.25.0",
+                "@esbuild/freebsd-arm64": "0.25.0",
+                "@esbuild/freebsd-x64": "0.25.0",
+                "@esbuild/linux-arm": "0.25.0",
+                "@esbuild/linux-arm64": "0.25.0",
+                "@esbuild/linux-ia32": "0.25.0",
+                "@esbuild/linux-loong64": "0.25.0",
+                "@esbuild/linux-mips64el": "0.25.0",
+                "@esbuild/linux-ppc64": "0.25.0",
+                "@esbuild/linux-riscv64": "0.25.0",
+                "@esbuild/linux-s390x": "0.25.0",
+                "@esbuild/linux-x64": "0.25.0",
+                "@esbuild/netbsd-arm64": "0.25.0",
+                "@esbuild/netbsd-x64": "0.25.0",
+                "@esbuild/openbsd-arm64": "0.25.0",
+                "@esbuild/openbsd-x64": "0.25.0",
+                "@esbuild/sunos-x64": "0.25.0",
+                "@esbuild/win32-arm64": "0.25.0",
+                "@esbuild/win32-ia32": "0.25.0",
+                "@esbuild/win32-x64": "0.25.0"
             }
         },
         "node_modules/escalade": {
@@ -7844,12 +7713,6 @@
                 "type": "^2.7.2"
             }
         },
-        "node_modules/extend": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-            "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-            "dev": true
-        },
         "node_modules/external-editor": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -7900,15 +7763,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/extsprintf": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-            "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-            "dev": true,
-            "engines": [
-                "node >=0.6.0"
-            ]
-        },
         "node_modules/eyes": {
             "version": "0.1.8",
             "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
@@ -7917,12 +7771,6 @@
                 "node": "> 0.1.90"
             }
         },
-        "node_modules/fast-deep-equal": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true
-        },
         "node_modules/fast-fifo": {
             "version": "1.3.2",
             "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -8208,15 +8056,6 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/forever-agent": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-            "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-            "dev": true,
-            "engines": {
-                "node": "*"
-            }
-        },
         "node_modules/form-data": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -8513,15 +8352,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/getpass": {
-            "version": "0.1.7",
-            "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-            "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-            "dev": true,
-            "dependencies": {
-                "assert-plus": "^1.0.0"
-            }
-        },
         "node_modules/glob": {
             "version": "10.4.5",
             "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -8571,66 +8401,6 @@
                 "node": ">=4"
             }
         },
-        "node_modules/globby": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
-            "integrity": "sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ==",
-            "dev": true,
-            "dependencies": {
-                "array-union": "^1.0.1",
-                "arrify": "^1.0.0",
-                "glob": "^7.0.3",
-                "object-assign": "^4.0.1",
-                "pify": "^2.0.0",
-                "pinkie-promise": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/globby/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/globby/node_modules/glob": {
-            "version": "7.2.3",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-            "deprecated": "Glob versions prior to v9 are no longer supported",
-            "dev": true,
-            "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.1.1",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            },
-            "engines": {
-                "node": "*"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/globby/node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
         "node_modules/gopd": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -8687,56 +8457,12 @@
                 "lodash": "^4.17.15"
             }
         },
-        "node_modules/har-schema": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-            "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/har-validator": {
-            "version": "5.1.5",
-            "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
-            "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
-            "deprecated": "this library is no longer supported",
-            "dev": true,
-            "dependencies": {
-                "ajv": "^6.12.3",
-                "har-schema": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
         "node_modules/harmony-reflect": {
             "version": "1.6.2",
             "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
             "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
             "dev": true
         },
-        "node_modules/has-ansi": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
-            "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
-            "dev": true,
-            "dependencies": {
-                "ansi-regex": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/has-ansi/node_modules/ansi-regex": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-            "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/has-bigints": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -8989,21 +8715,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/http-signature": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-            "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
-            "dev": true,
-            "dependencies": {
-                "assert-plus": "^1.0.0",
-                "jsprim": "^1.2.2",
-                "sshpk": "^1.7.0"
-            },
-            "engines": {
-                "node": ">=0.8",
-                "npm": ">=1.3.7"
-            }
-        },
         "node_modules/http2-wrapper": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -9185,12 +8896,6 @@
             "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
             "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
         },
-        "node_modules/ini": {
-            "version": "1.3.8",
-            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-            "dev": true
-        },
         "node_modules/inquirer": {
             "version": "11.1.0",
             "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz",
@@ -9468,39 +9173,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/is-path-cwd": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
-            "integrity": "sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-path-in-cwd": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
-            "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
-            "dev": true,
-            "dependencies": {
-                "is-path-inside": "^1.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-path-inside": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
-            "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==",
-            "dev": true,
-            "dependencies": {
-                "path-is-inside": "^1.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/is-plain-obj": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -9619,12 +9291,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
-            "dev": true
-        },
         "node_modules/is-unicode-supported": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -12198,24 +11864,6 @@
             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
             "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
         },
-        "node_modules/json-schema": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
-            "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-            "dev": true
-        },
-        "node_modules/json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-            "dev": true
-        },
-        "node_modules/json-stringify-safe": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-            "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-            "dev": true
-        },
         "node_modules/json5": {
             "version": "2.2.3",
             "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -12239,21 +11887,6 @@
                 "graceful-fs": "^4.1.6"
             }
         },
-        "node_modules/jsprim": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
-            "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
-            "dev": true,
-            "dependencies": {
-                "assert-plus": "1.0.0",
-                "extsprintf": "1.3.0",
-                "json-schema": "0.4.0",
-                "verror": "1.10.0"
-            },
-            "engines": {
-                "node": ">=0.6.0"
-            }
-        },
         "node_modules/jszip": {
             "version": "3.10.1",
             "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -13528,15 +13161,6 @@
             "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==",
             "dev": true
         },
-        "node_modules/oauth-sign": {
-            "version": "0.9.0",
-            "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-            "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
-            "dev": true,
-            "engines": {
-                "node": "*"
-            }
-        },
         "node_modules/object-assign": {
             "version": "4.1.1",
             "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -13918,12 +13542,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/path-is-inside": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
-            "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
-            "dev": true
-        },
         "node_modules/path-key": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -13990,12 +13608,6 @@
             "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
             "dev": true
         },
-        "node_modules/performance-now": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-            "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-            "dev": true
-        },
         "node_modules/picocolors": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -14012,36 +13624,6 @@
                 "url": "https://github.com/sponsors/jonschlinkert"
             }
         },
-        "node_modules/pify": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-            "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/pinkie": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
-            "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/pinkie-promise": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
-            "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
-            "dev": true,
-            "dependencies": {
-                "pinkie": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/pirates": {
             "version": "4.0.6",
             "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -14331,17 +13913,6 @@
                 }
             ]
         },
-        "node_modules/q": {
-            "version": "1.5.1",
-            "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
-            "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
-            "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
-            "dev": true,
-            "engines": {
-                "node": ">=0.6.0",
-                "teleport": ">=0.2.0"
-            }
-        },
         "node_modules/qs": {
             "version": "6.13.0",
             "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -14978,84 +14549,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/request": {
-            "version": "2.88.2",
-            "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
-            "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
-            "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
-            "dev": true,
-            "dependencies": {
-                "aws-sign2": "~0.7.0",
-                "aws4": "^1.8.0",
-                "caseless": "~0.12.0",
-                "combined-stream": "~1.0.6",
-                "extend": "~3.0.2",
-                "forever-agent": "~0.6.1",
-                "form-data": "~2.3.2",
-                "har-validator": "~5.1.3",
-                "http-signature": "~1.2.0",
-                "is-typedarray": "~1.0.0",
-                "isstream": "~0.1.2",
-                "json-stringify-safe": "~5.0.1",
-                "mime-types": "~2.1.19",
-                "oauth-sign": "~0.9.0",
-                "performance-now": "^2.1.0",
-                "qs": "~6.5.2",
-                "safe-buffer": "^5.1.2",
-                "tough-cookie": "~2.5.0",
-                "tunnel-agent": "^0.6.0",
-                "uuid": "^3.3.2"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/request/node_modules/form-data": {
-            "version": "2.3.3",
-            "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-            "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-            "dev": true,
-            "dependencies": {
-                "asynckit": "^0.4.0",
-                "combined-stream": "^1.0.6",
-                "mime-types": "^2.1.12"
-            },
-            "engines": {
-                "node": ">= 0.12"
-            }
-        },
-        "node_modules/request/node_modules/qs": {
-            "version": "6.5.3",
-            "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
-            "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.6"
-            }
-        },
-        "node_modules/request/node_modules/tough-cookie": {
-            "version": "2.5.0",
-            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-            "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-            "dev": true,
-            "dependencies": {
-                "psl": "^1.1.28",
-                "punycode": "^2.1.1"
-            },
-            "engines": {
-                "node": ">=0.8"
-            }
-        },
-        "node_modules/request/node_modules/uuid": {
-            "version": "3.4.0",
-            "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-            "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-            "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
-            "dev": true,
-            "bin": {
-                "uuid": "bin/uuid"
-            }
-        },
         "node_modules/require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -15168,62 +14661,6 @@
             "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==",
             "dev": true
         },
-        "node_modules/rimraf": {
-            "version": "2.7.1",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
-            "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
-            "deprecated": "Rimraf versions prior to v4 are no longer supported",
-            "dev": true,
-            "dependencies": {
-                "glob": "^7.1.3"
-            },
-            "bin": {
-                "rimraf": "bin.js"
-            }
-        },
-        "node_modules/rimraf/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/rimraf/node_modules/glob": {
-            "version": "7.2.3",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-            "deprecated": "Glob versions prior to v9 are no longer supported",
-            "dev": true,
-            "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.1.1",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            },
-            "engines": {
-                "node": "*"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/rimraf/node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
         "node_modules/rndm": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
@@ -15768,37 +15205,6 @@
             "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
             "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
         },
-        "node_modules/sshpk": {
-            "version": "1.18.0",
-            "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
-            "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
-            "dev": true,
-            "dependencies": {
-                "asn1": "~0.2.3",
-                "assert-plus": "^1.0.0",
-                "bcrypt-pbkdf": "^1.0.0",
-                "dashdash": "^1.12.0",
-                "ecc-jsbn": "~0.1.1",
-                "getpass": "^0.1.1",
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.0.2",
-                "tweetnacl": "~0.14.0"
-            },
-            "bin": {
-                "sshpk-conv": "bin/sshpk-conv",
-                "sshpk-sign": "bin/sshpk-sign",
-                "sshpk-verify": "bin/sshpk-verify"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/sshpk/node_modules/jsbn": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-            "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-            "dev": true
-        },
         "node_modules/stack-trace": {
             "version": "0.0.10",
             "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -16556,24 +15962,6 @@
                 "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
             }
         },
-        "node_modules/tunnel-agent": {
-            "version": "0.6.0",
-            "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-            "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-            "dev": true,
-            "dependencies": {
-                "safe-buffer": "^5.0.1"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/tweetnacl": {
-            "version": "0.14.5",
-            "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-            "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-            "dev": true
-        },
         "node_modules/type": {
             "version": "2.7.3",
             "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
@@ -16767,15 +16155,6 @@
                 "tslib": "^2.0.3"
             }
         },
-        "node_modules/uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "dependencies": {
-                "punycode": "^2.1.0"
-            }
-        },
         "node_modules/url": {
             "version": "0.10.3",
             "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
@@ -16888,26 +16267,6 @@
                 "node": ">= 0.8"
             }
         },
-        "node_modules/verror": {
-            "version": "1.10.0",
-            "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-            "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-            "dev": true,
-            "engines": [
-                "node >=0.6.0"
-            ],
-            "dependencies": {
-                "assert-plus": "^1.0.0",
-                "core-util-is": "1.0.2",
-                "extsprintf": "^1.2.0"
-            }
-        },
-        "node_modules/verror/node_modules/core-util-is": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-            "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-            "dev": true
-        },
         "node_modules/w3c-xmlserializer": {
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -17073,160 +16432,6 @@
                 "node": ">=18.20.0"
             }
         },
-        "node_modules/webdriver-manager": {
-            "version": "12.1.9",
-            "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
-            "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
-            "dev": true,
-            "dependencies": {
-                "adm-zip": "^0.5.2",
-                "chalk": "^1.1.1",
-                "del": "^2.2.0",
-                "glob": "^7.0.3",
-                "ini": "^1.3.4",
-                "minimist": "^1.2.0",
-                "q": "^1.4.1",
-                "request": "^2.87.0",
-                "rimraf": "^2.5.2",
-                "semver": "^5.3.0",
-                "xml2js": "^0.4.17"
-            },
-            "bin": {
-                "webdriver-manager": "bin/webdriver-manager"
-            },
-            "engines": {
-                "node": ">=6.9.x"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/ansi-regex": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-            "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/ansi-styles": {
-            "version": "2.2.1",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-            "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/chalk": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-            "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^2.2.1",
-                "escape-string-regexp": "^1.0.2",
-                "has-ansi": "^2.0.0",
-                "strip-ansi": "^3.0.0",
-                "supports-color": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/escape-string-regexp": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-            "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.8.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/glob": {
-            "version": "7.2.3",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-            "deprecated": "Glob versions prior to v9 are no longer supported",
-            "dev": true,
-            "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.1.1",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            },
-            "engines": {
-                "node": "*"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/semver": {
-            "version": "5.7.2",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
-            "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
-            "dev": true,
-            "bin": {
-                "semver": "bin/semver"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/strip-ansi": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-            "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
-            "dev": true,
-            "dependencies": {
-                "ansi-regex": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/supports-color": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-            "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.8.0"
-            }
-        },
-        "node_modules/webdriver-manager/node_modules/xml2js": {
-            "version": "0.4.23",
-            "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
-            "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
-            "dev": true,
-            "dependencies": {
-                "sax": ">=0.6.0",
-                "xmlbuilder": "~11.0.0"
-            },
-            "engines": {
-                "node": ">=4.0.0"
-            }
-        },
         "node_modules/webdriver/node_modules/@wdio/types": {
             "version": "9.2.2",
             "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.2.2.tgz",
@@ -18581,170 +17786,177 @@
             "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
         },
         "@esbuild/aix-ppc64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
-            "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
+            "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
             "dev": true,
             "optional": true
         },
         "@esbuild/android-arm": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
-            "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
+            "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
             "dev": true,
             "optional": true
         },
         "@esbuild/android-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
-            "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
+            "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
             "dev": true,
             "optional": true
         },
         "@esbuild/android-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
-            "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
+            "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/darwin-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
-            "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
+            "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/darwin-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
-            "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
+            "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/freebsd-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
-            "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
             "dev": true,
             "optional": true
         },
         "@esbuild/freebsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
-            "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
+            "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-arm": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
-            "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
+            "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
-            "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
+            "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-ia32": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
-            "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
+            "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-loong64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
-            "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
+            "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-mips64el": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
-            "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
+            "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-ppc64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
-            "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
+            "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-riscv64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
-            "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
+            "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-s390x": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
-            "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
+            "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
             "dev": true,
             "optional": true
         },
         "@esbuild/linux-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
-            "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
+            "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/netbsd-arm64": {
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/netbsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
-            "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
+            "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
             "dev": true,
             "optional": true
         },
         "@esbuild/openbsd-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
-            "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
+            "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/openbsd-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
-            "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
+            "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/sunos-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
-            "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
+            "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
             "dev": true,
             "optional": true
         },
         "@esbuild/win32-arm64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
-            "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
+            "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
             "dev": true,
             "optional": true
         },
         "@esbuild/win32-ia32": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
-            "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
+            "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
             "dev": true,
             "optional": true
         },
         "@esbuild/win32-x64": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
-            "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
+            "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
             "dev": true,
             "optional": true
         },
@@ -21078,12 +20290,6 @@
                 "acorn": "^8.11.0"
             }
         },
-        "adm-zip": {
-            "version": "0.5.14",
-            "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.14.tgz",
-            "integrity": "sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg==",
-            "dev": true
-        },
         "agent-base": {
             "version": "7.1.1",
             "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
@@ -21093,18 +20299,6 @@
                 "debug": "^4.3.4"
             }
         },
-        "ajv": {
-            "version": "6.12.6",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-            "dev": true,
-            "requires": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            }
-        },
         "ansi-colors": {
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -21323,48 +20517,12 @@
             "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
             "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
         },
-        "array-union": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
-            "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
-            "dev": true,
-            "requires": {
-                "array-uniq": "^1.0.1"
-            }
-        },
-        "array-uniq": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
-            "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
-            "dev": true
-        },
-        "arrify": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
-            "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
-            "dev": true
-        },
         "asap": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
             "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
             "dev": true
         },
-        "asn1": {
-            "version": "0.2.6",
-            "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-            "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-            "dev": true,
-            "requires": {
-                "safer-buffer": "~2.1.0"
-            }
-        },
-        "assert-plus": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-            "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-            "dev": true
-        },
         "ast-types": {
             "version": "0.13.4",
             "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -21422,18 +20580,6 @@
                 }
             }
         },
-        "aws-sign2": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-            "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-            "dev": true
-        },
-        "aws4": {
-            "version": "1.13.0",
-            "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz",
-            "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
-            "dev": true
-        },
         "axios": {
             "version": "1.7.8",
             "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
@@ -21632,15 +20778,6 @@
             "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
             "dev": true
         },
-        "bcrypt-pbkdf": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-            "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-            "dev": true,
-            "requires": {
-                "tweetnacl": "^0.14.3"
-            }
-        },
         "binary-extensions": {
             "version": "2.3.0",
             "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -21865,12 +21002,6 @@
                 "upper-case-first": "^2.0.2"
             }
         },
-        "caseless": {
-            "version": "0.12.0",
-            "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-            "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-            "dev": true
-        },
         "chalk": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -22332,18 +21463,18 @@
             "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ=="
         },
         "cookie-parser": {
-            "version": "1.4.6",
-            "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
-            "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+            "version": "1.4.7",
+            "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+            "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
             "requires": {
-                "cookie": "0.4.1",
+                "cookie": "0.7.2",
                 "cookie-signature": "1.0.6"
             },
             "dependencies": {
                 "cookie": {
-                    "version": "0.4.1",
-                    "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
-                    "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+                    "version": "0.7.2",
+                    "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+                    "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
                 }
             }
         },
@@ -22775,15 +21906,6 @@
                 "lodash": "^4.17.15"
             }
         },
-        "dashdash": {
-            "version": "1.14.1",
-            "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-            "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-            "dev": true,
-            "requires": {
-                "assert-plus": "^1.0.0"
-            }
-        },
         "data-uri-to-buffer": {
             "version": "4.0.1",
             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -22948,21 +22070,6 @@
                 "esprima": "^4.0.1"
             }
         },
-        "del": {
-            "version": "2.2.2",
-            "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
-            "integrity": "sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ==",
-            "dev": true,
-            "requires": {
-                "globby": "^5.0.0",
-                "is-path-cwd": "^1.0.0",
-                "is-path-in-cwd": "^1.0.0",
-                "object-assign": "^4.0.1",
-                "pify": "^2.0.0",
-                "pinkie-promise": "^2.0.0",
-                "rimraf": "^2.2.8"
-            }
-        },
         "delayed-stream": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -23153,24 +22260,6 @@
                 "wcwidth": "^1.0.1"
             }
         },
-        "ecc-jsbn": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-            "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-            "dev": true,
-            "requires": {
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.1.0"
-            },
-            "dependencies": {
-                "jsbn": {
-                    "version": "0.1.1",
-                    "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-                    "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-                    "dev": true
-                }
-            }
-        },
         "edge-paths": {
             "version": "3.0.5",
             "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz",
@@ -23393,35 +22482,36 @@
             }
         },
         "esbuild": {
-            "version": "0.23.1",
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
-            "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
-            "dev": true,
-            "requires": {
-                "@esbuild/aix-ppc64": "0.23.1",
-                "@esbuild/android-arm": "0.23.1",
-                "@esbuild/android-arm64": "0.23.1",
-                "@esbuild/android-x64": "0.23.1",
-                "@esbuild/darwin-arm64": "0.23.1",
-                "@esbuild/darwin-x64": "0.23.1",
-                "@esbuild/freebsd-arm64": "0.23.1",
-                "@esbuild/freebsd-x64": "0.23.1",
-                "@esbuild/linux-arm": "0.23.1",
-                "@esbuild/linux-arm64": "0.23.1",
-                "@esbuild/linux-ia32": "0.23.1",
-                "@esbuild/linux-loong64": "0.23.1",
-                "@esbuild/linux-mips64el": "0.23.1",
-                "@esbuild/linux-ppc64": "0.23.1",
-                "@esbuild/linux-riscv64": "0.23.1",
-                "@esbuild/linux-s390x": "0.23.1",
-                "@esbuild/linux-x64": "0.23.1",
-                "@esbuild/netbsd-x64": "0.23.1",
-                "@esbuild/openbsd-arm64": "0.23.1",
-                "@esbuild/openbsd-x64": "0.23.1",
-                "@esbuild/sunos-x64": "0.23.1",
-                "@esbuild/win32-arm64": "0.23.1",
-                "@esbuild/win32-ia32": "0.23.1",
-                "@esbuild/win32-x64": "0.23.1"
+            "version": "0.25.0",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
+            "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
+            "dev": true,
+            "requires": {
+                "@esbuild/aix-ppc64": "0.25.0",
+                "@esbuild/android-arm": "0.25.0",
+                "@esbuild/android-arm64": "0.25.0",
+                "@esbuild/android-x64": "0.25.0",
+                "@esbuild/darwin-arm64": "0.25.0",
+                "@esbuild/darwin-x64": "0.25.0",
+                "@esbuild/freebsd-arm64": "0.25.0",
+                "@esbuild/freebsd-x64": "0.25.0",
+                "@esbuild/linux-arm": "0.25.0",
+                "@esbuild/linux-arm64": "0.25.0",
+                "@esbuild/linux-ia32": "0.25.0",
+                "@esbuild/linux-loong64": "0.25.0",
+                "@esbuild/linux-mips64el": "0.25.0",
+                "@esbuild/linux-ppc64": "0.25.0",
+                "@esbuild/linux-riscv64": "0.25.0",
+                "@esbuild/linux-s390x": "0.25.0",
+                "@esbuild/linux-x64": "0.25.0",
+                "@esbuild/netbsd-arm64": "0.25.0",
+                "@esbuild/netbsd-x64": "0.25.0",
+                "@esbuild/openbsd-arm64": "0.25.0",
+                "@esbuild/openbsd-x64": "0.25.0",
+                "@esbuild/sunos-x64": "0.25.0",
+                "@esbuild/win32-arm64": "0.25.0",
+                "@esbuild/win32-ia32": "0.25.0",
+                "@esbuild/win32-x64": "0.25.0"
             }
         },
         "escalade": {
@@ -23652,12 +22742,6 @@
                 "type": "^2.7.2"
             }
         },
-        "extend": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-            "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-            "dev": true
-        },
         "external-editor": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -23692,23 +22776,11 @@
                 }
             }
         },
-        "extsprintf": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-            "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-            "dev": true
-        },
         "eyes": {
             "version": "0.1.8",
             "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
             "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="
         },
-        "fast-deep-equal": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true
-        },
         "fast-fifo": {
             "version": "1.3.2",
             "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -23918,12 +22990,6 @@
                 "signal-exit": "^4.0.1"
             }
         },
-        "forever-agent": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-            "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-            "dev": true
-        },
         "form-data": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -24132,15 +23198,6 @@
                 }
             }
         },
-        "getpass": {
-            "version": "0.1.7",
-            "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-            "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-            "dev": true,
-            "requires": {
-                "assert-plus": "^1.0.0"
-            }
-        },
         "glob": {
             "version": "10.4.5",
             "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -24178,55 +23235,6 @@
             "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
             "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
         },
-        "globby": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
-            "integrity": "sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ==",
-            "dev": true,
-            "requires": {
-                "array-union": "^1.0.1",
-                "arrify": "^1.0.0",
-                "glob": "^7.0.3",
-                "object-assign": "^4.0.1",
-                "pify": "^2.0.0",
-                "pinkie-promise": "^2.0.0"
-            },
-            "dependencies": {
-                "brace-expansion": {
-                    "version": "1.1.11",
-                    "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-                    "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-                    "dev": true,
-                    "requires": {
-                        "balanced-match": "^1.0.0",
-                        "concat-map": "0.0.1"
-                    }
-                },
-                "glob": {
-                    "version": "7.2.3",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-                    "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-                    "dev": true,
-                    "requires": {
-                        "fs.realpath": "^1.0.0",
-                        "inflight": "^1.0.4",
-                        "inherits": "2",
-                        "minimatch": "^3.1.1",
-                        "once": "^1.3.0",
-                        "path-is-absolute": "^1.0.0"
-                    }
-                },
-                "minimatch": {
-                    "version": "3.1.2",
-                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-                    "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-                    "dev": true,
-                    "requires": {
-                        "brace-expansion": "^1.1.7"
-                    }
-                }
-            }
-        },
         "gopd": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -24273,45 +23281,12 @@
                 "lodash": "^4.17.15"
             }
         },
-        "har-schema": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-            "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
-            "dev": true
-        },
-        "har-validator": {
-            "version": "5.1.5",
-            "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
-            "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
-            "dev": true,
-            "requires": {
-                "ajv": "^6.12.3",
-                "har-schema": "^2.0.0"
-            }
-        },
         "harmony-reflect": {
             "version": "1.6.2",
             "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
             "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
             "dev": true
         },
-        "has-ansi": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
-            "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
-            "dev": true,
-            "requires": {
-                "ansi-regex": "^2.0.0"
-            },
-            "dependencies": {
-                "ansi-regex": {
-                    "version": "2.1.1",
-                    "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-                    "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
-                    "dev": true
-                }
-            }
-        },
         "has-bigints": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -24503,17 +23478,6 @@
                 "debug": "^4.3.4"
             }
         },
-        "http-signature": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-            "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
-            "dev": true,
-            "requires": {
-                "assert-plus": "^1.0.0",
-                "jsprim": "^1.2.2",
-                "sshpk": "^1.7.0"
-            }
-        },
         "http2-wrapper": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -24645,12 +23609,6 @@
             "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
             "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
         },
-        "ini": {
-            "version": "1.3.8",
-            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-            "dev": true
-        },
         "inquirer": {
             "version": "11.1.0",
             "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz",
@@ -24839,30 +23797,6 @@
                 "has-tostringtag": "^1.0.0"
             }
         },
-        "is-path-cwd": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
-            "integrity": "sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==",
-            "dev": true
-        },
-        "is-path-in-cwd": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
-            "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
-            "dev": true,
-            "requires": {
-                "is-path-inside": "^1.0.0"
-            }
-        },
-        "is-path-inside": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
-            "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==",
-            "dev": true,
-            "requires": {
-                "path-is-inside": "^1.0.1"
-            }
-        },
         "is-plain-obj": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -24932,12 +23866,6 @@
                 "which-typed-array": "^1.1.14"
             }
         },
-        "is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
-            "dev": true
-        },
         "is-unicode-supported": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -26818,24 +25746,6 @@
             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
             "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
         },
-        "json-schema": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
-            "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-            "dev": true
-        },
-        "json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-            "dev": true
-        },
-        "json-stringify-safe": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-            "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
-            "dev": true
-        },
         "json5": {
             "version": "2.2.3",
             "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -26851,18 +25761,6 @@
                 "universalify": "^2.0.0"
             }
         },
-        "jsprim": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
-            "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
-            "dev": true,
-            "requires": {
-                "assert-plus": "1.0.0",
-                "extsprintf": "1.3.0",
-                "json-schema": "0.4.0",
-                "verror": "1.10.0"
-            }
-        },
         "jszip": {
             "version": "3.10.1",
             "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -27814,12 +26712,6 @@
             "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==",
             "dev": true
         },
-        "oauth-sign": {
-            "version": "0.9.0",
-            "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-            "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
-            "dev": true
-        },
         "object-assign": {
             "version": "4.1.1",
             "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -28090,12 +26982,6 @@
             "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
             "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
         },
-        "path-is-inside": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
-            "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
-            "dev": true
-        },
         "path-key": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -28151,12 +27037,6 @@
             "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
             "dev": true
         },
-        "performance-now": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-            "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-            "dev": true
-        },
         "picocolors": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -28167,27 +27047,6 @@
             "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
             "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
         },
-        "pify": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-            "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-            "dev": true
-        },
-        "pinkie": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
-            "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
-            "dev": true
-        },
-        "pinkie-promise": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
-            "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
-            "dev": true,
-            "requires": {
-                "pinkie": "^2.0.0"
-            }
-        },
         "pirates": {
             "version": "4.0.6",
             "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -28382,12 +27241,6 @@
             "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
             "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="
         },
-        "q": {
-            "version": "1.5.1",
-            "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
-            "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
-            "dev": true
-        },
         "qs": {
             "version": "6.13.0",
             "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -28846,69 +27699,6 @@
                 "set-function-name": "^2.0.1"
             }
         },
-        "request": {
-            "version": "2.88.2",
-            "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
-            "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
-            "dev": true,
-            "requires": {
-                "aws-sign2": "~0.7.0",
-                "aws4": "^1.8.0",
-                "caseless": "~0.12.0",
-                "combined-stream": "~1.0.6",
-                "extend": "~3.0.2",
-                "forever-agent": "~0.6.1",
-                "form-data": "~2.3.2",
-                "har-validator": "~5.1.3",
-                "http-signature": "~1.2.0",
-                "is-typedarray": "~1.0.0",
-                "isstream": "~0.1.2",
-                "json-stringify-safe": "~5.0.1",
-                "mime-types": "~2.1.19",
-                "oauth-sign": "~0.9.0",
-                "performance-now": "^2.1.0",
-                "qs": "~6.5.2",
-                "safe-buffer": "^5.1.2",
-                "tough-cookie": "~2.5.0",
-                "tunnel-agent": "^0.6.0",
-                "uuid": "^3.3.2"
-            },
-            "dependencies": {
-                "form-data": {
-                    "version": "2.3.3",
-                    "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-                    "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-                    "dev": true,
-                    "requires": {
-                        "asynckit": "^0.4.0",
-                        "combined-stream": "^1.0.6",
-                        "mime-types": "^2.1.12"
-                    }
-                },
-                "qs": {
-                    "version": "6.5.3",
-                    "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
-                    "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
-                    "dev": true
-                },
-                "tough-cookie": {
-                    "version": "2.5.0",
-                    "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-                    "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-                    "dev": true,
-                    "requires": {
-                        "psl": "^1.1.28",
-                        "punycode": "^2.1.1"
-                    }
-                },
-                "uuid": {
-                    "version": "3.4.0",
-                    "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-                    "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-                    "dev": true
-                }
-            }
-        },
         "require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -28997,50 +27787,6 @@
             "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==",
             "dev": true
         },
-        "rimraf": {
-            "version": "2.7.1",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
-            "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
-            "dev": true,
-            "requires": {
-                "glob": "^7.1.3"
-            },
-            "dependencies": {
-                "brace-expansion": {
-                    "version": "1.1.11",
-                    "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-                    "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-                    "dev": true,
-                    "requires": {
-                        "balanced-match": "^1.0.0",
-                        "concat-map": "0.0.1"
-                    }
-                },
-                "glob": {
-                    "version": "7.2.3",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-                    "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-                    "dev": true,
-                    "requires": {
-                        "fs.realpath": "^1.0.0",
-                        "inflight": "^1.0.4",
-                        "inherits": "2",
-                        "minimatch": "^3.1.1",
-                        "once": "^1.3.0",
-                        "path-is-absolute": "^1.0.0"
-                    }
-                },
-                "minimatch": {
-                    "version": "3.1.2",
-                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-                    "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-                    "dev": true,
-                    "requires": {
-                        "brace-expansion": "^1.1.7"
-                    }
-                }
-            }
-        },
         "rndm": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
@@ -29465,31 +28211,6 @@
             "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
             "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
         },
-        "sshpk": {
-            "version": "1.18.0",
-            "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
-            "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
-            "dev": true,
-            "requires": {
-                "asn1": "~0.2.3",
-                "assert-plus": "^1.0.0",
-                "bcrypt-pbkdf": "^1.0.0",
-                "dashdash": "^1.12.0",
-                "ecc-jsbn": "~0.1.1",
-                "getpass": "^0.1.1",
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.0.2",
-                "tweetnacl": "~0.14.0"
-            },
-            "dependencies": {
-                "jsbn": {
-                    "version": "0.1.1",
-                    "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-                    "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-                    "dev": true
-                }
-            }
-        },
         "stack-trace": {
             "version": "0.0.10",
             "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -30053,7 +28774,7 @@
             "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
             "dev": true,
             "requires": {
-                "esbuild": "~0.23.0",
+                "esbuild": "0.25.0",
                 "fsevents": "~2.3.3",
                 "get-tsconfig": "^4.7.5"
             }
@@ -30064,21 +28785,6 @@
             "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
             "dev": true
         },
-        "tunnel-agent": {
-            "version": "0.6.0",
-            "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-            "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-            "dev": true,
-            "requires": {
-                "safe-buffer": "^5.0.1"
-            }
-        },
-        "tweetnacl": {
-            "version": "0.14.5",
-            "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-            "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-            "dev": true
-        },
         "type": {
             "version": "2.7.3",
             "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
@@ -30199,15 +28905,6 @@
                 "tslib": "^2.0.3"
             }
         },
-        "uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "requires": {
-                "punycode": "^2.1.0"
-            }
-        },
         "url": {
             "version": "0.10.3",
             "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
@@ -30304,25 +29001,6 @@
             "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
             "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
         },
-        "verror": {
-            "version": "1.10.0",
-            "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-            "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-            "dev": true,
-            "requires": {
-                "assert-plus": "^1.0.0",
-                "core-util-is": "1.0.2",
-                "extsprintf": "^1.2.0"
-            },
-            "dependencies": {
-                "core-util-is": {
-                    "version": "1.0.2",
-                    "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-                    "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-                    "dev": true
-                }
-            }
-        },
         "w3c-xmlserializer": {
             "version": "5.0.0",
             "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -30461,122 +29139,6 @@
                 }
             }
         },
-        "webdriver-manager": {
-            "version": "12.1.9",
-            "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
-            "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
-            "dev": true,
-            "requires": {
-                "adm-zip": "^0.5.2",
-                "chalk": "^1.1.1",
-                "del": "^2.2.0",
-                "glob": "^7.0.3",
-                "ini": "^1.3.4",
-                "minimist": "^1.2.0",
-                "q": "^1.4.1",
-                "request": "^2.87.0",
-                "rimraf": "^2.5.2",
-                "semver": "^5.3.0",
-                "xml2js": "^0.4.17"
-            },
-            "dependencies": {
-                "ansi-regex": {
-                    "version": "2.1.1",
-                    "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-                    "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
-                    "dev": true
-                },
-                "ansi-styles": {
-                    "version": "2.2.1",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-                    "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
-                    "dev": true
-                },
-                "brace-expansion": {
-                    "version": "1.1.11",
-                    "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-                    "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-                    "dev": true,
-                    "requires": {
-                        "balanced-match": "^1.0.0",
-                        "concat-map": "0.0.1"
-                    }
-                },
-                "chalk": {
-                    "version": "1.1.3",
-                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-                    "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^2.2.1",
-                        "escape-string-regexp": "^1.0.2",
-                        "has-ansi": "^2.0.0",
-                        "strip-ansi": "^3.0.0",
-                        "supports-color": "^2.0.0"
-                    }
-                },
-                "escape-string-regexp": {
-                    "version": "1.0.5",
-                    "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-                    "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-                    "dev": true
-                },
-                "glob": {
-                    "version": "7.2.3",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-                    "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-                    "dev": true,
-                    "requires": {
-                        "fs.realpath": "^1.0.0",
-                        "inflight": "^1.0.4",
-                        "inherits": "2",
-                        "minimatch": "^3.1.1",
-                        "once": "^1.3.0",
-                        "path-is-absolute": "^1.0.0"
-                    }
-                },
-                "minimatch": {
-                    "version": "3.1.2",
-                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-                    "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-                    "dev": true,
-                    "requires": {
-                        "brace-expansion": "^1.1.7"
-                    }
-                },
-                "semver": {
-                    "version": "5.7.2",
-                    "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
-                    "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
-                    "dev": true
-                },
-                "strip-ansi": {
-                    "version": "3.0.1",
-                    "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-                    "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
-                    "dev": true,
-                    "requires": {
-                        "ansi-regex": "^2.0.0"
-                    }
-                },
-                "supports-color": {
-                    "version": "2.0.0",
-                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-                    "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
-                    "dev": true
-                },
-                "xml2js": {
-                    "version": "0.4.23",
-                    "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
-                    "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
-                    "dev": true,
-                    "requires": {
-                        "sax": ">=0.6.0",
-                        "xmlbuilder": "~11.0.0"
-                    }
-                }
-            }
-        },
         "webdriverio": {
             "version": "9.4.1",
             "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index 1f737df6f..33d042405 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -61,7 +61,7 @@
         "axios": "1.7.8",
         "body-parser": "1.20.3",
         "cookie": "0.7.0",
-        "cookie-parser": "1.4.6",
+        "cookie-parser": "1.4.7",
         "cookie-session": "2.0.0",
         "csurf": "1.11.0",
         "cytoscape": "3.22.1",
@@ -139,7 +139,9 @@
         "react-test-renderer": "18.2.0",
         "sinon": "14.0.0",
         "supertest": "7.0.0",
-        "webdriver-manager": "12.1.9",
         "webdriverio": "9.4.1"
+    },
+    "overrides": {
+        "esbuild": "0.25.0"
     }
 }
diff --git a/ui/pom.xml b/ui/pom.xml
index 49180dc00..62731c447 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-      <version>1.12.11-SNAPSHOT</version>
+      <version>1.12.12-SNAPSHOT</version>
     <relativePath>../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/athenz-conf/pom.xml b/utils/athenz-conf/pom.xml
index 4a27bb151..ed4d97266 100644
--- a/utils/athenz-conf/pom.xml
+++ b/utils/athenz-conf/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/msd-agent/pom.xml b/utils/msd-agent/pom.xml
index 9f09b431a..37ebd298f 100644
--- a/utils/msd-agent/pom.xml
+++ b/utils/msd-agent/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zms-cli/pom.xml b/utils/zms-cli/pom.xml
index a261df6dd..bcff88653 100644
--- a/utils/zms-cli/pom.xml
+++ b/utils/zms-cli/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zms-domainattrs/pom.xml b/utils/zms-domainattrs/pom.xml
index c1b961e6e..722d25025 100644
--- a/utils/zms-domainattrs/pom.xml
+++ b/utils/zms-domainattrs/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zms-svctoken/pom.xml b/utils/zms-svctoken/pom.xml
index 8e35eddf2..b33993705 100644
--- a/utils/zms-svctoken/pom.xml
+++ b/utils/zms-svctoken/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zpe-updater/pom.xml b/utils/zpe-updater/pom.xml
index f1ffd1f67..afe30ce98 100644
--- a/utils/zpe-updater/pom.xml
+++ b/utils/zpe-updater/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zts-accesstoken/pom.xml b/utils/zts-accesstoken/pom.xml
index aa8def8b2..2a1441be6 100644
--- a/utils/zts-accesstoken/pom.xml
+++ b/utils/zts-accesstoken/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zts-idtoken/pom.xml b/utils/zts-idtoken/pom.xml
index dcff3cc03..f245f76cc 100644
--- a/utils/zts-idtoken/pom.xml
+++ b/utils/zts-idtoken/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zts-rolecert/pom.xml b/utils/zts-rolecert/pom.xml
index 8884b5b91..2dc72fe8c 100644
--- a/utils/zts-rolecert/pom.xml
+++ b/utils/zts-rolecert/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zts-roletoken/pom.xml b/utils/zts-roletoken/pom.xml
index 20c325aa1..fd23b247e 100644
--- a/utils/zts-roletoken/pom.xml
+++ b/utils/zts-roletoken/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 
diff --git a/utils/zts-svccert/pom.xml b/utils/zts-svccert/pom.xml
index c30ee098b..8c01b5fae 100644
--- a/utils/zts-svccert/pom.xml
+++ b/utils/zts-svccert/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>com.yahoo.athenz</groupId>
     <artifactId>athenz</artifactId>
-    <version>1.12.11-SNAPSHOT</version>
+    <version>1.12.12-SNAPSHOT</version>
     <relativePath>../../pom.xml</relativePath>
   </parent>
 



Test patch:
diff --git a/clients/go/zts/token/access_token_test.go b/clients/go/zts/token/access_token_test.go
new file mode 100644
index 000000000..6384d3302
--- /dev/null
+++ b/clients/go/zts/token/access_token_test.go
@@ -0,0 +1,249 @@
+package token
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+	"github.com/patrickmn/go-cache"
+)
+
+func TestMakeTokenRequest(t *testing.T) {
+	tests := []struct {
+		name     string
+		domain   string
+		roles    []string
+		expiry   int
+		proxy    string
+		expected map[string]string
+	}{
+		{
+			name:   "role is * yields domain scope",
+			domain: "example.com",
+			roles:  []string{"*"},
+			expiry: 3600,
+			proxy:  "",
+			expected: map[string]string{
+				"grant_type": "client_credentials",
+				"expires_in": "3600",
+				"scope":      "example.com:domain",
+			},
+		},
+		{
+			name:   "single role",
+			domain: "example.com",
+			roles:  []string{"admin"},
+			expiry: 3600,
+			proxy:  "",
+			expected: map[string]string{
+				"grant_type": "client_credentials",
+				"expires_in": "3600",
+				"scope":      "example.com:role.admin",
+			},
+		},
+		{
+			name:   "multiple roles",
+			domain: "example.com",
+			roles:  []string{"admin", "user"},
+			expiry: 3600,
+			proxy:  "",
+			expected: map[string]string{
+				"grant_type": "client_credentials",
+				"expires_in": "3600",
+				"scope":      "example.com:role.admin example.com:role.user",
+			},
+		},
+		{
+			name:   "with proxy",
+			domain: "example.com",
+			roles:  []string{"admin"},
+			expiry: 3600,
+			proxy:  "spiffe://proxy",
+			expected: map[string]string{
+				"grant_type":                  "client_credentials",
+				"expires_in":                  "3600",
+				"scope":                       "example.com:role.admin",
+				"proxy_principal_spiffe_uris": "spiffe://proxy",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			query := makeTokenRequest(tt.domain, tt.roles, tt.expiry, tt.proxy)
+			values, err := url.ParseQuery(query)
+			if err != nil {
+				t.Fatalf("failed to parse query: %v", err)
+			}
+			for key, expVal := range tt.expected {
+				if got := values.Get(key); got != expVal {
+					t.Errorf("for key %q, expected %q, got %q", key, expVal, got)
+				}
+			}
+		})
+	}
+}
+
+func TestNewAccessTokenClient(t *testing.T) {
+	tests := []struct {
+		name string
+		ctx  context.Context
+	}{
+		{
+			name: "nil context",
+			ctx:  nil,
+		},
+		{
+			name: "non-nil context",
+			ctx:  context.Background(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client, cancel := NewAccessTokenClient(tt.ctx, "http://dummy", "dummy.pem", "dummy.key")
+			defer cancel()
+			if client == nil {
+				t.Fatal("expected non-nil AccessTokenClient")
+			}
+			if cancel == nil {
+				t.Error("expected non-nil cancel function")
+			}
+		})
+	}
+}
+
+func TestNewAccessTokenClientSetCacheUpdateDuration(t *testing.T) {
+	tests := []struct {
+		name string
+		ctx  context.Context
+	}{
+		{
+			name: "nil context, 1 hour update duration",
+			ctx:  nil,
+		},
+		{
+			name: "non-nil context, 30 minutes update duration",
+			ctx:  context.Background(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client, cancel := NewAccessTokenClientSetCacheUpdateDuration(tt.ctx, "http://dummy", "dummy.pem", "dummy.key", 1*time.Hour)
+			defer cancel()
+			if client == nil {
+				t.Fatal("expected non-nil AccessTokenClient")
+			}
+			if client.BaseTokenClient == nil {
+				t.Error("expected non-nil BaseTokenClient")
+			}
+			baseCli := client.BaseTokenClient.Client
+			if baseCli == nil {
+				t.Error("expected non-nil Client inside BaseTokenClient")
+			}
+			if baseCli.Cache == nil {
+				t.Error("expected non-nil Cache in Client")
+			}
+			if baseCli.ZTS == nil {
+				t.Error("expected non-nil ZTS in Client")
+			}
+			if baseCli.Tok != client {
+				t.Error("expected Client.Tok to be equal to the created AccessTokenClient")
+			}
+			if client.BaseTokenClient.getTokenFunc == nil {
+				t.Error("expected non-nil getTokenFunc")
+			}
+		})
+	}
+}
+
+func TestNewAccessTokenClientWrapper(t *testing.T) {
+	client, cancel := NewAccessTokenClient(context.Background(), "http://dummy", "dummy.pem", "dummy.key")
+	defer cancel()
+	if client == nil {
+		t.Error("expected non-nil AccessTokenClient")
+	}
+}
+
+func TestAccessTokenClient_Callback(t *testing.T) {
+	origNewClientFunc := newClientFunc
+	defer func() { newClientFunc = origNewClientFunc }()
+	now := time.Now().Unix()
+	tests := []struct {
+		name        string
+		accessToken *zts.AccessTokenResponse
+		fakeErr     error
+		fakeToken   string
+		fakeExpiry  int64
+		expectError bool
+		expectToken string
+	}{
+		{
+			name: "successful callback",
+			accessToken: &zts.AccessTokenResponse{
+				Access_token: "accessTokenSuccess",
+				Expires_in: func() *int32 {
+					exp := int32(3600)
+					return &exp
+				}(),
+			},
+			fakeErr:     nil,
+			fakeExpiry:  now + 3600,
+			expectError: false,
+			expectToken: "accessTokenSuccess",
+		},
+		{
+			name:        "error callback",
+			accessToken: nil,
+			fakeErr:     fmt.Errorf("simulated error"),
+			fakeToken:   "",
+			fakeExpiry:  0,
+			expectError: true,
+			expectToken: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeZTS := &fakeZTSClient{
+				AccessTokenResponse: tt.accessToken,
+				Error:               tt.fakeErr,
+			}
+			newClientFunc = func(url, pem, key string) *Client {
+				return &Client{
+					ZTS:   fakeZTS,
+					Cache: cache.New(5*time.Minute, 10*time.Minute),
+				}
+			}
+			atClient, cancel := NewAccessTokenClientSetCacheUpdateDuration(context.Background(), "http://dummy", "dummy.pem", "dummy.key", 1*time.Hour)
+			defer cancel()
+
+			accessToken, err := atClient.BaseTokenClient.getTokenFunc("dummyDomain", "admin", 3600)
+			if tt.expectError {
+				if err == nil {
+					t.Error("expected error, got nil")
+				}
+				if accessToken != nil {
+					t.Error("expected nil roleToken on error")
+				}
+			} else {
+				if err != nil {
+					t.Errorf("unexpected error: %v", err)
+				}
+				if accessToken == nil {
+					t.Fatal("expected non-nil roleToken")
+				}
+				if accessToken.Token.Access_token != tt.expectToken {
+					t.Errorf("expected token %q, got %q", tt.expectToken, accessToken.Token.Access_token)
+				}
+				if accessToken.ExpiryTime != tt.fakeExpiry {
+					t.Errorf("expected expiry time %d, got %d", tt.fakeExpiry, accessToken.ExpiryTime)
+				}
+			}
+		})
+	}
+}
diff --git a/clients/go/zts/token/client_test.go b/clients/go/zts/token/client_test.go
new file mode 100644
index 000000000..e3cd0b9f5
--- /dev/null
+++ b/clients/go/zts/token/client_test.go
@@ -0,0 +1,821 @@
+package token
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"fmt"
+	"log/slog"
+	"math/big"
+	"net/http"
+	"os"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"testing"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+	"github.com/patrickmn/go-cache"
+)
+
+type fakeZTSClient struct {
+	URLField            string
+	TransportField      *http.Transport
+	AccessTokenResponse *zts.AccessTokenResponse
+	RoleTokenResponse   *zts.RoleToken
+	Error               error
+}
+
+func (f *fakeZTSClient) PostAccessTokenRequest(req zts.AccessTokenRequest) (*zts.AccessTokenResponse, error) {
+	return f.AccessTokenResponse, f.Error
+}
+
+func (f *fakeZTSClient) GetRoleToken(domain zts.DomainName, roles zts.EntityList, minExpiryTime *int32, maxExpiryTime *int32, proxyForPrincipal zts.EntityName) (*zts.RoleToken, error) {
+	return f.RoleTokenResponse, f.Error
+}
+
+func (f *fakeZTSClient) SetTransport(t *http.Transport) {
+	f.TransportField = t
+}
+
+func (f *fakeZTSClient) Transport() *http.Transport {
+	return f.TransportField
+}
+
+func (f *fakeZTSClient) URL() string {
+	return ""
+}
+
+func (f *fakeZTSClient) SetURL(url string) {
+	f.URLField = url
+}
+
+type GetTokenFunc func(domain string, roles string, exp int32) (interface{}, error)
+
+func (f GetTokenFunc) getToken(domain string, roles string, exp int32) (interface{}, error) {
+	return f(domain, roles, exp)
+}
+
+func createTempCertFiles(t *testing.T) (certPath, keyPath string) {
+	t.Helper()
+	priv, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("failed to generate private key: %v", err)
+	}
+	template := x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject: pkix.Name{
+			Organization: []string{"Test Org"},
+		},
+		NotBefore:             time.Now().Add(-time.Hour),
+		NotAfter:              time.Now().Add(time.Hour),
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+	}
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
+	if err != nil {
+		t.Fatalf("failed to create certificate: %v", err)
+	}
+	certFile, err := os.CreateTemp("", "testcert_*.pem")
+	if err != nil {
+		t.Fatalf("failed to create temp cert file: %v", err)
+	}
+	defer certFile.Close()
+	if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
+		t.Fatalf("failed to write cert file: %v", err)
+	}
+	keyFile, err := os.CreateTemp("", "testkey_*.pem")
+	if err != nil {
+		t.Fatalf("failed to create temp key file: %v", err)
+	}
+	defer keyFile.Close()
+	if err := pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
+		t.Fatalf("failed to write key file: %v", err)
+	}
+	return certFile.Name(), keyFile.Name()
+}
+
+func TestNewClient(t *testing.T) {
+	validCert, validKey := createTempCertFiles(t)
+	defer os.Remove(validCert)
+	defer os.Remove(validKey)
+
+	tests := []struct {
+		name         string
+		url          string
+		certPath     string
+		keyPath      string
+		expectTLSCfg bool
+	}{
+		{
+			name:         "Valid cert and key",
+			url:          "http://example.com",
+			certPath:     validCert,
+			keyPath:      validKey,
+			expectTLSCfg: true,
+		},
+		{
+			name:         "Invalid cert and key",
+			url:          "http://example.com",
+			certPath:     "nonexistent_cert.pem",
+			keyPath:      "nonexistent_key.pem",
+			expectTLSCfg: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := NewClient(tt.url, tt.certPath, tt.keyPath)
+			if client == nil {
+				t.Fatal("Expected non-nil Client")
+			}
+			if client.Cache == nil {
+				t.Error("Expected non-nil Cache")
+			}
+			if client.ZTS == nil {
+				t.Error("Expected non-nil ZTS client")
+			}
+			tlsCfg := client.ZTS.Transport().TLSClientConfig
+			if tt.expectTLSCfg {
+				if tlsCfg == nil {
+					t.Error("Expected TLSClientConfig to be set")
+				}
+			} else {
+				if tlsCfg != nil {
+					t.Error("Expected TLSClientConfig to be nil for invalid files")
+				}
+			}
+		})
+	}
+}
+
+func TestGetCacheKey(t *testing.T) {
+	tests := []struct {
+		name     string
+		domain   string
+		roles    []string
+		exp      int32
+		expected string
+	}{
+		{"Single role", "example", []string{"admin"}, 3600, "example:admin:3600"},
+		{"Multiple roles unsorted", "example", []string{"user", "admin"}, 3600, "example:admin,user:3600"},
+		{"Empty role", "example", []string{""}, 0, "example::0"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			key := getCacheKey(tt.domain, tt.roles, tt.exp)
+			if key != tt.expected {
+				t.Errorf("Expected cache key %q, got %q", tt.expected, key)
+			}
+		})
+	}
+}
+
+func TestCreateTLSConfig(t *testing.T) {
+	validCert, validKey := createTempCertFiles(t)
+	defer os.Remove(validCert)
+	defer os.Remove(validKey)
+
+	tests := []struct {
+		name        string
+		certPath    string
+		keyPath     string
+		expectError bool
+		expectCerts bool
+	}{
+		{"Valid files", validCert, validKey, false, true},
+		{"Invalid files", "nonexistent_cert.pem", "nonexistent_key.pem", true, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tlsCfg, err := createTLSConfig(tt.certPath, tt.keyPath)
+			if tt.expectError {
+				if err == nil {
+					t.Error("Expected error, got nil")
+				}
+			} else {
+				if err != nil {
+					t.Errorf("Unexpected error: %v", err)
+				}
+				if tlsCfg == nil {
+					t.Error("Expected non-nil TLS config")
+				} else if tt.expectCerts && len(tlsCfg.Certificates) == 0 {
+					t.Error("Expected certificates in TLS config")
+				}
+			}
+		})
+	}
+}
+
+func TestSetTLSConfig(t *testing.T) {
+	tests := []struct {
+		name             string
+		initialTransport *http.Transport
+		newCfg           *tls.Config
+		expectInsecure   bool
+	}{
+		{
+			name: "Transport not nil: update from false to true",
+			initialTransport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+			},
+			newCfg:         &tls.Config{InsecureSkipVerify: true},
+			expectInsecure: true,
+		},
+		{
+			name: "Transport not nil: update from true to false",
+			initialTransport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			},
+			newCfg:         &tls.Config{InsecureSkipVerify: false},
+			expectInsecure: false,
+		},
+		{
+			name:             "Transport is nil: new transport is created with new TLS config",
+			initialTransport: nil,
+			newCfg:           &tls.Config{InsecureSkipVerify: true},
+			expectInsecure:   true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fake := &fakeZTSClient{
+				TransportField: tt.initialTransport,
+			}
+			c := &Client{
+				ZTS:   fake,
+				Cache: cache.New(5*time.Minute, 10*time.Minute),
+			}
+			c.SetTLSConfig(tt.newCfg)
+			tr := c.ZTS.Transport()
+			if tr == nil {
+				t.Fatal("Expected non-nil Transport after SetTLSConfig")
+			}
+			if tr.TLSClientConfig == nil {
+				t.Fatal("Expected non-nil TLSClientConfig after SetTLSConfig")
+			}
+			if tr.TLSClientConfig.InsecureSkipVerify != tt.expectInsecure {
+				t.Errorf("Expected InsecureSkipVerify %v, got %v", tt.expectInsecure, tr.TLSClientConfig.InsecureSkipVerify)
+			}
+		})
+	}
+}
+
+func TestTransport(t *testing.T) {
+	fake := &fakeZTSClient{
+		TransportField: &http.Transport{},
+	}
+	c := &Client{
+		ZTS:   fake,
+		Cache: cache.New(5*time.Minute, 10*time.Minute),
+	}
+	if tr := c.Transport(); tr != fake.TransportField {
+		t.Errorf("Expected transport %v, got %v", fake.TransportField, tr)
+	}
+}
+
+func TestUpdateCachePeriodically(t *testing.T) {
+	tests := []struct {
+		name         string
+		getter       func(domain string, roles string, exp int32) (interface{}, error)
+		expectUpdate bool
+	}{
+		{
+			name: "Successful update",
+			getter: func(domain string, roles string, exp int32) (interface{}, error) {
+				return &AccessToken{
+					Token: &zts.AccessTokenResponse{
+						Access_token: "newToken",
+					},
+					ExpiryTime: time.Now().Unix() + 600,
+				}, nil
+			},
+			expectUpdate: true,
+		},
+		{
+			name: "Error fetching new token",
+			getter: func(domain string, roles string, exp int32) (interface{}, error) {
+				return nil, fmt.Errorf("simulated error")
+			},
+			expectUpdate: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				Cache: cache.New(5*time.Minute, 10*time.Minute),
+			}
+			key := getCacheKey("test", []string{"role"}, 3600)
+			oldToken := &AccessToken{
+				Token: &zts.AccessTokenResponse{
+					Access_token: "oldToken",
+				},
+				ExpiryTime: time.Now().Unix() + 100,
+			}
+			c.Cache.Set(key, oldToken, 200*time.Second)
+
+			ctx, cancel := context.WithCancel(context.Background())
+			var wg sync.WaitGroup
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				c.UpdateCachePeriodically(ctx, GetTokenFunc(tt.getter), 100*time.Millisecond)
+			}()
+
+			time.Sleep(300 * time.Millisecond)
+			cancel()
+			wg.Wait()
+
+			cached, found := c.Cache.Get(key)
+			if !found {
+				t.Fatal("Cache key not found")
+			}
+			updatedToken, ok := cached.(*AccessToken)
+			if !ok {
+				t.Fatal("Cached item is not *AccessToken")
+			}
+
+			if tt.expectUpdate {
+				if updatedToken.Token.Access_token == "oldToken" {
+					t.Error("Expected token to be updated, but it remains unchanged")
+				}
+			} else {
+				if updatedToken.Token.Access_token != "oldToken" {
+					t.Error("Expected token to remain unchanged due to error, but it was updated")
+				}
+			}
+		})
+	}
+}
+
+type filePairFunc func(t *testing.T) (cert string, key string, cleanup func())
+
+func validFilePair(t *testing.T) (string, string, func()) {
+	cert, key := createTempCertFiles(t)
+	cleanup := func() {
+		os.Remove(cert)
+		os.Remove(key)
+	}
+	return cert, key, cleanup
+}
+func invalidFilePair(t *testing.T) (string, string, func()) {
+	createInvalid := func() (string, func()) {
+		f, err := os.CreateTemp("", "invalid_*.pem")
+		if err != nil {
+			t.Fatalf("failed to create temp file: %v", err)
+		}
+		content := []byte("invalid content")
+		if err := os.WriteFile(f.Name(), content, 0644); err != nil {
+			t.Fatalf("failed to write invalid content: %v", err)
+		}
+		return f.Name(), func() { os.Remove(f.Name()) }
+	}
+	cert, cleanupCert := createInvalid()
+	key, cleanupKey := createInvalid()
+	cleanup := func() {
+		cleanupCert()
+		cleanupKey()
+	}
+	return cert, key, cleanup
+}
+func filePairCertMissing(t *testing.T) (string, string, func()) {
+	_, key, cleanup := validFilePair(t)
+	return "nonexistent_cert.pem", key, cleanup
+}
+
+func TestWatchCertificateFiles(t *testing.T) {
+	tests := []struct {
+		name                string
+		filePair            filePairFunc
+		modifyEvent         bool
+		expectedErrContains string
+	}{
+		{
+			name:                "immediate cancellation with valid files",
+			filePair:            validFilePair,
+			modifyEvent:         false,
+			expectedErrContains: "",
+		},
+		{
+			name:                "file add error (cert file missing)",
+			filePair:            filePairCertMissing,
+			modifyEvent:         false,
+			expectedErrContains: "failed to watch file",
+		},
+		{
+			name:                "valid file event with valid TLS config",
+			filePair:            validFilePair,
+			modifyEvent:         true,
+			expectedErrContains: "",
+		},
+		{
+			name:                "valid file event with invalid TLS config",
+			filePair:            invalidFilePair,
+			modifyEvent:         true,
+			expectedErrContains: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cert, key, cleanup := tt.filePair(t)
+			defer cleanup()
+
+			client := &Client{
+				Cache: cache.New(5*time.Minute, 10*time.Minute),
+				ZTS:   &fakeZTSClient{TransportField: &http.Transport{}},
+			}
+
+			ctx, cancel := context.WithCancel(context.Background())
+			errCh := make(chan error, 1)
+			go func() {
+				errCh <- client.WatchCertificateFiles(ctx, cert, key)
+			}()
+
+			if tt.modifyEvent {
+				time.Sleep(100 * time.Millisecond)
+				content, err := os.ReadFile(cert)
+				if err != nil {
+					t.Fatalf("failed to read cert file: %v", err)
+				}
+				if err := os.WriteFile(cert, content, 0644); err != nil {
+					t.Fatalf("failed to rewrite cert file: %v", err)
+				}
+			}
+
+			time.Sleep(200 * time.Millisecond)
+			cancel()
+
+			err := <-errCh
+			if tt.expectedErrContains != "" {
+				if err == nil {
+					t.Errorf("expected error containing %q, got nil", tt.expectedErrContains)
+				} else if !strings.Contains(err.Error(), tt.expectedErrContains) {
+					t.Errorf("expected error containing %q, got %q", tt.expectedErrContains, err.Error())
+				}
+			} else {
+				if err != nil {
+					t.Errorf("expected no error, got %q", err.Error())
+				}
+			}
+		})
+	}
+}
+
+type dummyTransport struct{}
+
+func (dt *dummyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	return nil, nil
+}
+
+func TestStartBackgroundTasks(t *testing.T) {
+	dummyPem, dummyKey := createTempCertFiles(t)
+	defer os.Remove(dummyPem)
+	defer os.Remove(dummyKey)
+
+	tests := []struct {
+		name         string
+		ctx          context.Context
+		certPath     string
+		keyPath      string
+		watcherError bool
+		expectedLog  string
+	}{
+		{
+			name:         "nil context",
+			ctx:          nil,
+			certPath:     dummyPem,
+			keyPath:      dummyKey,
+			watcherError: false,
+			expectedLog:  "",
+		},
+		{
+			name:         "non-nil context",
+			ctx:          context.Background(),
+			certPath:     dummyPem,
+			keyPath:      dummyKey,
+			watcherError: false,
+			expectedLog:  "",
+		},
+		{
+			name:         "invalid cert/key path",
+			ctx:          context.Background(),
+			certPath:     "invalid_cert_path",
+			keyPath:      "invalid_key_path",
+			watcherError: true,
+			expectedLog:  "Error watching certificate files:",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			var buf bytes.Buffer
+			handler := slog.NewTextHandler(&buf, nil)
+			testLogger := slog.New(handler)
+			oldLogger := slog.Default()
+			slog.SetDefault(testLogger)
+			defer slog.SetDefault(oldLogger)
+
+			client := NewClient("http://example.com", dummyPem, dummyKey)
+			cancel := client.startBackgroundTasks(tt.ctx, tt.certPath, tt.keyPath, 100*time.Millisecond)
+			if cancel == nil {
+				t.Error("Expected non-nil cancel function")
+			}
+			time.Sleep(200 * time.Millisecond)
+			cancel()
+			time.Sleep(100 * time.Millisecond)
+
+			logOutput := buf.String()
+			if tt.watcherError {
+				if !strings.Contains(logOutput, tt.expectedLog) {
+					t.Errorf("expected log to contain %q, but got %q", tt.expectedLog, logOutput)
+				}
+			}
+		})
+	}
+}
+
+func TestSetProxy(t *testing.T) {
+	tests := []struct {
+		name             string
+		initialTransport *http.Transport
+		proxyURL         string
+		expectSuccess    bool
+	}{
+		{
+			name:             "Transport not nil, valid proxy",
+			initialTransport: &http.Transport{},
+			proxyURL:         "http://proxy.example.com:8080",
+			expectSuccess:    true,
+		},
+		{
+			name:             "Transport is nil, valid proxy",
+			initialTransport: nil,
+			proxyURL:         "http://proxy.example.com:8080",
+			expectSuccess:    true,
+		},
+		{
+			name:             "Transport not nil, invalid proxy",
+			initialTransport: &http.Transport{},
+			proxyURL:         "://invalid-proxy",
+			expectSuccess:    false,
+		},
+		{
+			name:             "Transport is nil, invalid proxy",
+			initialTransport: nil,
+			proxyURL:         "://invalid-proxy",
+			expectSuccess:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fake := &fakeZTSClient{
+				TransportField: tt.initialTransport,
+			}
+			c := &Client{
+				ZTS:   fake,
+				Cache: cache.New(5*time.Minute, 10*time.Minute),
+			}
+
+			c.SetProxy(tt.proxyURL)
+			newTransport := c.ZTS.Transport()
+
+			if tt.expectSuccess {
+				if newTransport == nil {
+					t.Fatal("Expected non-nil transport after SetProxy")
+				}
+				req, err := http.NewRequest("GET", "http://example.com", nil)
+				if err != nil {
+					t.Fatalf("Error creating request: %v", err)
+				}
+				proxy, err := newTransport.Proxy(req)
+				if err != nil {
+					t.Fatalf("Unexpected error calling Proxy: %v", err)
+				}
+				if proxy == nil {
+					t.Fatal("Expected proxy to be set, got nil")
+				}
+				if proxy.String() != tt.proxyURL {
+					t.Errorf("Expected proxy URL %q, got %q", tt.proxyURL, proxy.String())
+				}
+			} else {
+				if tt.initialTransport == nil {
+					if newTransport != nil {
+						t.Errorf("Expected transport to remain nil for invalid proxy, got non-nil")
+					}
+				} else {
+					if newTransport != tt.initialTransport {
+						t.Errorf("Expected transport to remain unchanged for invalid proxy")
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestGetTokenWithExpire(t *testing.T) {
+	domain := "testdomain"
+	roles := []string{"admin"}
+	exp := 14400
+	now := time.Now().Unix()
+	cacheKey := getCacheKey(domain, roles, int32(exp))
+
+	tests := []struct {
+		name                 string
+		initialCacheToken    *AccessToken
+		getTokenFunc         func(domain string, roles string, exp int32) (*AccessToken, error)
+		expectedToken        string
+		expectedExpiry       int64
+		expectError          bool
+		expectGetTokenCalled bool
+	}{
+		{
+			name: "Cache hit with valid token",
+			initialCacheToken: &AccessToken{
+				Token: &zts.AccessTokenResponse{
+					Access_token: "cachedToken",
+				},
+				ExpiryTime: now + int64(exp),
+				Duration:   14400 * time.Second,
+			},
+			getTokenFunc: func(domain string, roles string, exp int32) (*AccessToken, error) {
+				t.Fatal("getTokenFunc should not be called in cache hit case")
+				return nil, nil
+			},
+			expectedToken:        "cachedToken",
+			expectedExpiry:       now + int64(exp),
+			expectError:          false,
+			expectGetTokenCalled: false,
+		},
+		{
+			name:              "Cache miss with getTokenFunc success",
+			initialCacheToken: nil,
+			getTokenFunc: func(domain string, roles string, exp int32) (*AccessToken, error) {
+				return &AccessToken{
+					Token: &zts.AccessTokenResponse{
+						Access_token: "newToken",
+					},
+					ExpiryTime: now + int64(exp),
+					Duration:   14400 * time.Second,
+				}, nil
+			},
+			expectedToken:        "newToken",
+			expectedExpiry:       now + int64(exp),
+			expectError:          false,
+			expectGetTokenCalled: true,
+		},
+		{
+			name:              "Cache miss with getTokenFunc error and no cache",
+			initialCacheToken: nil,
+			getTokenFunc: func(domain string, roles string, exp int32) (*AccessToken, error) {
+				return nil, fmt.Errorf("simulated error")
+			},
+			expectedToken:        "",
+			expectError:          true,
+			expectGetTokenCalled: true,
+		},
+		{
+			name: "Cache present (expired) with getTokenFunc error",
+			initialCacheToken: &AccessToken{
+				Token: &zts.AccessTokenResponse{
+					Access_token: "cachedTokenExpired",
+				},
+				ExpiryTime: now + 1000,
+				Duration:   3000 * time.Second,
+			},
+			getTokenFunc: func(domain string, roles string, exp int32) (*AccessToken, error) {
+				return nil, fmt.Errorf("simulated error")
+			},
+			expectedToken:        "cachedTokenExpired",
+			expectedExpiry:       now + 1000,
+			expectError:          true,
+			expectGetTokenCalled: true,
+		},
+		{
+			name: "Cache present (expired) with getTokenFunc success",
+			initialCacheToken: &AccessToken{
+				Token: &zts.AccessTokenResponse{
+					Access_token: "cachedTokenExpired",
+				},
+				ExpiryTime: now + 1000,
+				Duration:   3000 * time.Second,
+			},
+			getTokenFunc: func(domain string, roles string, exp int32) (*AccessToken, error) {
+				return &AccessToken{
+					Token: &zts.AccessTokenResponse{
+						Access_token: "newTokenOverride",
+					},
+					ExpiryTime: time.Now().Unix() + int64(exp),
+					Duration:   14400 * time.Second,
+				}, nil
+			},
+			expectedToken:        "newTokenOverride",
+			expectedExpiry:       now + int64(exp),
+			expectError:          false,
+			expectGetTokenCalled: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var callCount int32
+			wrappedFunc := func(domain string, roles string, exp int32) (*AccessToken, error) {
+				atomic.AddInt32(&callCount, 1)
+				return tt.getTokenFunc(domain, roles, exp)
+			}
+
+			c := cache.New(5*time.Minute, 10*time.Minute)
+			if tt.initialCacheToken != nil {
+				c.Set(cacheKey, tt.initialCacheToken, 1*time.Hour)
+			}
+			client := &Client{Cache: c}
+			btc := &BaseTokenClient[*AccessToken]{
+				Client:       client,
+				getTokenFunc: wrappedFunc,
+			}
+
+			result, err := btc.GetTokenWithExpire(domain, roles, int32(exp))
+			if tt.expectError {
+				if err == nil {
+					t.Error("expected error, got nil")
+				}
+				if tt.initialCacheToken == nil {
+					if result != nil {
+						t.Error("expected nil result on error with no cache")
+					}
+				} else {
+					if result == nil {
+						t.Error("expected cached token, got nil")
+					} else if result.GetToken().Access_token != tt.expectedToken {
+						t.Errorf("expected token %q, got %q", tt.expectedToken, result.GetToken().Access_token)
+					}
+				}
+			} else {
+				if err != nil {
+					t.Errorf("unexpected error: %v", err)
+				}
+				if result == nil {
+					t.Fatal("result is nil")
+				}
+				if result.GetToken().Access_token != tt.expectedToken {
+					t.Errorf("expected token %q, got %q", tt.expectedToken, result.GetToken().Access_token)
+				}
+
+				if tt.expectedExpiry != result.GetExpiryTime() {
+					t.Errorf("expected expiry time %d, but got %d", tt.expectedExpiry, result.GetExpiryTime())
+				}
+			}
+
+			called := atomic.LoadInt32(&callCount) > 0
+			if called != tt.expectGetTokenCalled {
+				t.Errorf("expected getTokenFunc called = %v, got %v", tt.expectGetTokenCalled, called)
+			}
+		})
+	}
+}
+
+func TestGetToken(t *testing.T) {
+	domain := "testdomain"
+	roles := []string{"admin"}
+	var callCount int32
+	wrappedFunc := func(domain string, roles string, exp int32) (*AccessToken, error) {
+		atomic.AddInt32(&callCount, 1)
+		return &AccessToken{
+			Token: &zts.AccessTokenResponse{
+				Access_token: "newToken",
+			},
+			ExpiryTime: time.Now().Unix() + 3600,
+		}, nil
+	}
+
+	c := cache.New(5*time.Minute, 10*time.Minute)
+	client := &Client{Cache: c}
+	btc := &BaseTokenClient[*AccessToken]{
+		Client:       client,
+		getTokenFunc: wrappedFunc,
+	}
+
+	result, err := btc.GetToken(domain, roles)
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+	if result == nil {
+		t.Fatal("result is nil")
+	}
+	if result.GetToken().Access_token != "newToken" {
+		t.Errorf("expected token 'newToken', got %q", result.GetToken().Access_token)
+	}
+	if atomic.LoadInt32(&callCount) == 0 {
+		t.Error("expected getTokenFunc to be called")
+	}
+}
diff --git a/clients/go/zts/token/role_token_test.go b/clients/go/zts/token/role_token_test.go
new file mode 100644
index 000000000..7d1573886
--- /dev/null
+++ b/clients/go/zts/token/role_token_test.go
@@ -0,0 +1,137 @@
+package token
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+	"github.com/patrickmn/go-cache"
+)
+
+func TestNewRoleTokenClientSetCacheUpdateDuration(t *testing.T) {
+	tests := []struct {
+		name string
+		ctx  context.Context
+	}{
+		{
+			name: "nil context",
+			ctx:  nil,
+		},
+		{
+			name: "non-nil context",
+			ctx:  context.Background(),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			rtClient, cancel := NewRoleTokenClientSetCacheUpdateDuration(tt.ctx, "http://dummy", "dummy.pem", "dummy.key", 1*time.Hour)
+			defer cancel()
+			if rtClient == nil {
+				t.Fatal("expected non-nil RoleTokenClient")
+			}
+			if rtClient.BaseTokenClient == nil {
+				t.Error("expected non-nil BaseTokenClient")
+			}
+			baseCli := rtClient.BaseTokenClient.Client
+			if baseCli == nil {
+				t.Error("expected non-nil Client")
+			}
+			if baseCli.Cache == nil {
+				t.Error("expected non-nil Cache in Client")
+			}
+			if baseCli.ZTS == nil {
+				t.Error("expected non-nil ZTS in Client")
+			}
+			if baseCli.Tok != rtClient {
+				t.Error("expected Client.Tok to equal the created RoleTokenClient")
+			}
+		})
+	}
+}
+
+func TestNewRoleTokenClientWrapper(t *testing.T) {
+	client, cancel := NewRoleTokenClient(context.Background(), "http://dummy", "dummy.pem", "dummy.key")
+	defer cancel()
+	if client == nil {
+		t.Error("expected non-nil AccessTokenClient")
+	}
+}
+
+func TestRoleTokenClient_Callback(t *testing.T) {
+	origNewClientFunc := newClientFunc
+	defer func() { newClientFunc = origNewClientFunc }()
+	now := time.Now().Unix()
+	tests := []struct {
+		name        string
+		roleToken   *zts.RoleToken
+		fakeErr     error
+		fakeToken   string
+		fakeExpiry  int64
+		expectError bool
+		expectToken string
+	}{
+		{
+			name: "successful callback",
+			roleToken: &zts.RoleToken{
+				Token:      "roleTokenSuccess",
+				ExpiryTime: now + 3600,
+			},
+			fakeErr:     nil,
+			fakeExpiry:  now + 3600,
+			expectError: false,
+			expectToken: "roleTokenSuccess",
+		},
+		{
+			name:        "error callback",
+			roleToken:   nil,
+			fakeErr:     fmt.Errorf("simulated error"),
+			fakeToken:   "",
+			fakeExpiry:  0,
+			expectError: true,
+			expectToken: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeZTS := &fakeZTSClient{
+				RoleTokenResponse: tt.roleToken,
+				Error:             tt.fakeErr,
+			}
+			newClientFunc = func(url, pem, key string) *Client {
+				return &Client{
+					ZTS:   fakeZTS,
+					Cache: cache.New(5*time.Minute, 10*time.Minute),
+				}
+			}
+			rtClient, cancel := NewRoleTokenClientSetCacheUpdateDuration(context.Background(), "http://dummy", "dummy.pem", "dummy.key", 1*time.Hour)
+			defer cancel()
+
+			roleToken, err := rtClient.BaseTokenClient.getTokenFunc("dummyDomain", "admin", 3600)
+			if tt.expectError {
+				if err == nil {
+					t.Error("expected error, got nil")
+				}
+				if roleToken != nil {
+					t.Error("expected nil roleToken on error")
+				}
+			} else {
+				if err != nil {
+					t.Errorf("unexpected error: %v", err)
+				}
+				if roleToken == nil {
+					t.Fatal("expected non-nil roleToken")
+				}
+				if roleToken.Token.Token != tt.expectToken {
+					t.Errorf("expected token %q, got %q", tt.expectToken, roleToken.Token)
+				}
+				if roleToken.ExpiryTime != tt.fakeExpiry {
+					t.Errorf("expected expiry time %d, got %d", tt.fakeExpiry, roleToken.ExpiryTime)
+				}
+			}
+		})
+	}
+}
diff --git a/clients/go/zts/token/token_test.go b/clients/go/zts/token/token_test.go
new file mode 100644
index 000000000..f41577a23
--- /dev/null
+++ b/clients/go/zts/token/token_test.go
@@ -0,0 +1,46 @@
+package token
+
+import (
+	"testing"
+	"time"
+
+	"github.com/AthenZ/athenz/clients/go/zts"
+)
+
+func TestAccessToken_Getters(t *testing.T) {
+	expectedToken := "abc123"
+	expectedExpiry := time.Now().Unix() + 3600
+
+	at := &AccessToken{
+		Token: &zts.AccessTokenResponse{
+			Access_token: expectedToken,
+		},
+		ExpiryTime: expectedExpiry,
+	}
+
+	if at.GetToken().Access_token != expectedToken {
+		t.Errorf("AccessToken.GetToken: expected %q, got %q", expectedToken, at.GetToken().Access_token)
+	}
+	if at.GetExpiryTime() != expectedExpiry {
+		t.Errorf("AccessToken.GetExpiryTime: expected %d, got %d", expectedExpiry, at.GetExpiryTime())
+	}
+}
+
+func TestRoleToken_Getters(t *testing.T) {
+	expectedToken := "roleTokenXYZ"
+	expectedExpiry := time.Now().Unix() + 1800
+
+	rt := &RoleToken{
+		Token: &zts.RoleToken{
+			Token: expectedToken,
+		},
+		ExpiryTime: expectedExpiry,
+	}
+
+	if rt.GetToken().Token != expectedToken {
+		t.Errorf("RoleToken.GetToken: expected %q, got %q", expectedToken, rt.GetToken().Token)
+	}
+	if rt.GetExpiryTime() != expectedExpiry {
+		t.Errorf("RoleToken.GetExpiryTime: expected %d, got %d", expectedExpiry, rt.GetExpiryTime())
+	}
+}
diff --git a/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSClientTest.java b/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSClientTest.java
index 656b7750f..59beb068e 100644
--- a/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSClientTest.java
+++ b/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSClientTest.java
@@ -3172,42 +3172,48 @@ public class ZTSClientTest {
     }
 
     @Test
-    public void testGenerateAccessTokenRequestBody() throws UnsupportedEncodingException {
+    public void testGenerateAccessTokenRequestBody() {
 
         Principal principal = SimplePrincipal.create("user_domain", "user",
                 "auth_creds", PRINCIPAL_AUTHORITY);
         ZTSClient client = new ZTSClient("http://localhost:4080", principal);
 
-        assertEquals(client.generateAccessTokenRequestBody("coretech", null, null, null, null, null, 0),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", null, null, null, null, null, null, null, 0),
                 "grant_type=client_credentials&scope=coretech%3Adomain");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", null, null, null, null, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", null, null, null, null, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Adomain");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", null, "", null, null, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", null, "", null, null, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Adomain");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", null, "api", null, null, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", null, "api", null, null, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Adomain+openid+coretech%3Aservice.api");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api", null, null, "", 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api",
+                        null, null, "", null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+openid+coretech%3Aservice.api");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api", "", null, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"),
+                        "api", "", null, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+openid+coretech%3Aservice.api");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api", "user.proxy", null, "", 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"),
+                        "api", "user.proxy", null, "", null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+openid+coretech%3Aservice.api&proxy_for_principal=user.proxy");
         List<String> roles = new ArrayList<>();
         roles.add("readers");
         roles.add("writers");
-        assertEquals(client.generateAccessTokenRequestBody("coretech", roles, "api", null, null, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", roles, "api", null, null, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+coretech%3Arole.writers+openid+coretech%3Aservice.api");
         final String authorizationDetails = "[{\"type\":\"message_access\",\"location\":[\"https://location1\"," +
                 "\"https://location2\"],\"identifier\":\"id1\"}]";
         final String encodedDetails = "%5B%7B%22type%22%3A%22message_access%22%2C%22location%22%3A%5B%22https%3A%2F%2Flocation1%22%2C%22https%3A%2F%2Flocation2%22%5D%2C%22identifier%22%3A%22id1%22%7D%5D";
-        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api", null, authorizationDetails, null, 100),
+        assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"),
+                        "api", null, authorizationDetails, null, null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+openid+coretech%3Aservice.api&authorization_details=" + encodedDetails);
         final String proxyPrincipalsEncoded = "spiffe%3A%2F%2Fathenz%2Fsa%2Fservice1%2Cspiffe%3A%2F%2Fathenz%2Fsa%2Fservice2";
         assertEquals(client.generateAccessTokenRequestBody("coretech", Collections.singletonList("readers"), "api", null,
-                authorizationDetails, "spiffe://athenz/sa/service1,spiffe://athenz/sa/service2", 100),
+                authorizationDetails, "spiffe://athenz/sa/service1,spiffe://athenz/sa/service2", null, null, 100),
                 "grant_type=client_credentials&expires_in=100&scope=coretech%3Arole.readers+openid+coretech%3Aservice.api&authorization_details="
                                 + encodedDetails + "&proxy_principal_spiffe_uris=" + proxyPrincipalsEncoded);
-
+        assertEquals(client.generateAccessTokenRequestBody("coretech", null, null, null, null, null,
+                        "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "jwt", 0),
+                "grant_type=client_credentials&scope=coretech%3Adomain&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=jwt");
         client.close();
     }
 
@@ -3393,6 +3399,13 @@ public class ZTSClientTest {
         assertEquals(accessTokenResponse.getId_token(), "idtoken");
         assertEquals((int) accessTokenResponse.getExpires_in(), 3600);
 
+        // now with client assertion
+
+        accessTokenResponse = client.getAccessToken("coretech", null, null, null, null, null,
+                "jwt-bearer", "assertion", 2400, true);
+        assertNotNull(accessTokenResponse);
+        assertEquals(accessTokenResponse.getAccess_token(), "accesstoken");
+        assertEquals((int) accessTokenResponse.getExpires_in(), 2400);
 
         ZTSClient.setCacheDisable(true);
         accessTokenResponse = client.getAccessToken("coretech", null, "backend", 3600, true);
@@ -4122,6 +4135,50 @@ public class ZTSClientTest {
         }
     }
 
+    @Test
+    public void testPostIntrospectRequest() {
+
+        Principal principal = SimplePrincipal.create("user_domain", "user",
+                "auth_creds", PRINCIPAL_AUTHORITY);
+
+        ZTSRDLClientMock ztsClientMock = new ZTSRDLClientMock();
+        ZTSClient client = new ZTSClient("http://localhost:4080", principal);
+        client.setZTSRDLGeneratedClient(ztsClientMock);
+
+        IntrospectResponse response = client.introspectToken("accesstoken-coretch-role1");
+        assertTrue(response.getActive());
+        assertEquals(response.getScope(), "coretech:role.role1");
+
+        response = client.introspectToken("accessoken-expired");
+        assertFalse(response.getActive());
+
+        response = client.introspectToken("invalid");
+        assertFalse(response.getActive());
+
+        try {
+            client.introspectToken("introspect-disabled");
+            fail();
+        } catch (ZTSClientException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            client.introspectToken("unknown");
+            fail();
+        } catch (ZTSClientException ex) {
+            assertEquals(ex.getCode(), 404);
+        }
+
+        try {
+            client.introspectToken("exception");
+            fail();
+        } catch (ZTSClientException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        client.close();
+    }
+
     @Test
     public void testConstructorWithValidProxyUrl() {
         ServiceIdentityProvider siaProvider = Mockito.mock(ServiceIdentityProvider.class);
diff --git a/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSRDLClientMock.java b/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSRDLClientMock.java
index a87727539..df2422c1f 100644
--- a/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSRDLClientMock.java
+++ b/clients/java/zts/src/test/java/com/yahoo/athenz/zts/ZTSRDLClientMock.java
@@ -184,6 +184,28 @@ public class ZTSRDLClientMock extends ZTSRDLGeneratedClient implements java.io.C
         return roleToken;
     }
 
+    @Override
+    public IntrospectResponse postIntrospectRequest(String request) {
+        IntrospectResponse response = new IntrospectResponse();
+        switch (request) {
+            case "token=accesstoken-coretch-role1":
+                response.setActive(true);
+                response.setScope("coretech:role.role1");
+                break;
+            case "token=accessoken-expired":
+            case "token=invalid":
+                response.setActive(false);
+                break;
+            case "token=introspect-disabled":
+                throw new ClientResourceException(400, "introspection disabled");
+            case "token=exception":
+                throw new IllegalArgumentException("introspection exception");
+            default:
+                throw new ClientResourceException(404, "domain not found");
+        }
+        return response;
+    }
+
     @Override
     public AccessTokenResponse postAccessTokenRequest(String request) {
 
@@ -208,6 +230,10 @@ public class ZTSRDLClientMock extends ZTSRDLGeneratedClient implements java.io.C
                 tokenResponse.setScope("coretech2:role.role1");
                 tokenResponse.setExpires_in(8);
                 break;
+            case "grant_type=client_credentials&expires_in=2400&scope=coretech%3Adomain&client_assertion_type=jwt-bearer&client_assertion=assertion":
+                tokenResponse.setScope("coretech:role.role3");
+                tokenResponse.setExpires_in(2400);
+                break;
             case "grant_type=client_credentials&expires_in=3600&scope=coretech%3Adomain+openid+coretech%3Aservice.backend":
                 tokenResponse.setScope("coretech:role.role1");
                 tokenResponse.setId_token("idtoken");
diff --git a/core/zts/src/test/java/com/yahoo/athenz/zts/IntrospectResponseTest.java b/core/zts/src/test/java/com/yahoo/athenz/zts/IntrospectResponseTest.java
new file mode 100644
index 000000000..fdd1f5506
--- /dev/null
+++ b/core/zts/src/test/java/com/yahoo/athenz/zts/IntrospectResponseTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yahoo.athenz.zts;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+public class IntrospectResponseTest {
+
+    @Test
+    public void testIntrospectResponse() {
+
+        IntrospectResponse resp1 = new IntrospectResponse();
+        IntrospectResponse resp2 = new IntrospectResponse();
+
+        resp1.setActive(true);
+        resp1.setAud("aud");
+        resp1.setClient_id("client-id");
+        resp1.setExp(100L);
+        resp1.setIat(100L);
+        resp1.setIss("iss");
+        resp1.setJti("jti");
+        resp1.setScope("scope");
+        resp1.setSub("sub");
+        resp1.setAuth_time(100L);
+        resp1.setAuthorization_details("details");
+        resp1.setProxy("proxy");
+        resp1.setUid("uid");
+        resp1.setVer(1);
+
+        resp2.setActive(true);
+        resp2.setAud("aud");
+        resp2.setClient_id("client-id");
+        resp2.setExp(100L);
+        resp2.setIat(100L);
+        resp2.setIss("iss");
+        resp2.setJti("jti");
+        resp2.setScope("scope");
+        resp2.setSub("sub");
+        resp2.setAuth_time(100L);
+        resp2.setAuthorization_details("details");
+        resp2.setProxy("proxy");
+        resp2.setUid("uid");
+        resp2.setVer(1);
+
+        assertEquals(resp1, resp2);
+        assertEquals(resp1, resp1);
+        assertNotEquals(null, resp1);
+        assertNotEquals("InstrospectResponse", resp1);
+
+        assertTrue(resp1.getActive());
+        assertEquals(resp1.getAud(), "aud");
+        assertEquals(resp1.getClient_id(), "client-id");
+        assertEquals(resp1.getExp(), 100L);
+        assertEquals(resp1.getIat(), 100L);
+        assertEquals(resp1.getIss(), "iss");
+        assertEquals(resp1.getJti(), "jti");
+        assertEquals(resp1.getScope(), "scope");
+        assertEquals(resp1.getSub(), "sub");
+        assertEquals(resp1.getAuth_time(), 100L);
+        assertEquals(resp1.getAuthorization_details(), "details");
+        assertEquals(resp1.getProxy(), "proxy");
+        assertEquals(resp1.getUid(), "uid");
+        assertEquals(resp1.getVer(), 1);
+
+        resp2.setAud("aud2");
+        assertNotEquals(resp1, resp2);
+        resp2.setAud(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setAud("aud");
+
+        resp2.setClient_id("client-id2");
+        assertNotEquals(resp1, resp2);
+        resp2.setClient_id(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setClient_id("client-id");
+
+        resp2.setExp(200L);
+        assertNotEquals(resp1, resp2);
+        resp2.setExp(100L);
+
+        resp2.setIat(200L);
+        assertNotEquals(resp1, resp2);
+        resp2.setIat(100L);
+
+        resp2.setIss("iss2");
+        assertNotEquals(resp1, resp2);
+        resp2.setIss(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setIss("iss");
+
+        resp2.setJti("jti2");
+        assertNotEquals(resp1, resp2);
+        resp2.setJti(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setJti("jti");
+
+        resp2.setScope("scope2");
+        assertNotEquals(resp1, resp2);
+        resp2.setScope(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setScope("scope");
+
+        resp2.setSub("sub2");
+        assertNotEquals(resp1, resp2);
+        resp2.setSub(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setSub("sub");
+
+        resp2.setAuth_time(200L);
+        assertNotEquals(resp1, resp2);
+        resp2.setAuth_time(100L);
+
+        resp2.setAuthorization_details("details2");
+        assertNotEquals(resp1, resp2);
+        resp2.setAuthorization_details(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setAuthorization_details("details");
+
+        resp2.setProxy("proxy2");
+        assertNotEquals(resp1, resp2);
+        resp2.setProxy(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setProxy("proxy");
+
+        resp2.setUid("uid2");
+        assertNotEquals(resp1, resp2);
+        resp2.setUid(null);
+        assertNotEquals(resp1, resp2);
+        resp2.setUid("uid");
+
+        resp2.setVer(2);
+        assertNotEquals(resp1, resp2);
+        resp2.setVer(1);
+
+        resp2.setActive(false);
+        assertNotEquals(resp1, resp2);
+        resp2.setActive(true);
+    }
+}
diff --git a/core/zts/src/test/java/com/yahoo/athenz/zts/OAuthConfigTest.java b/core/zts/src/test/java/com/yahoo/athenz/zts/OAuthConfigTest.java
index 4d210716e..b2ab54e7d 100644
--- a/core/zts/src/test/java/com/yahoo/athenz/zts/OAuthConfigTest.java
+++ b/core/zts/src/test/java/com/yahoo/athenz/zts/OAuthConfigTest.java
@@ -31,6 +31,7 @@ public class OAuthConfigTest {
         OAuthConfig cfg1 = new OAuthConfig();
         OAuthConfig cfg2 = new OAuthConfig();
 
+        cfg1.setIntrospection_endpoint("introspect-endpoint");
         cfg1.setAuthorization_endpoint("authz-endpoint");
         cfg1.setToken_endpoint("token-endpoint");
         cfg1.setIssuer("issuer");
@@ -40,6 +41,7 @@ public class OAuthConfigTest {
         cfg1.setResponse_types_supported(Collections.singletonList("token"));
 
         cfg2.setAuthorization_endpoint("authz-endpoint");
+        cfg2.setIntrospection_endpoint("introspect-endpoint");
         cfg2.setToken_endpoint("token-endpoint");
         cfg2.setIssuer("issuer");
         cfg2.setJwks_uri("jwks-uri");
@@ -52,10 +54,11 @@ public class OAuthConfigTest {
         assertNotEquals(null, cfg1);
         assertNotEquals("OAuthconfig", cfg1);
 
-        assertEquals("authz-endpoint", cfg1.getAuthorization_endpoint());
-        assertEquals("token-endpoint", cfg1.getToken_endpoint());
-        assertEquals("issuer", cfg1.getIssuer());
-        assertEquals("jwks-uri", cfg1.getJwks_uri());
+        assertEquals(cfg1.getAuthorization_endpoint(), "authz-endpoint");
+        assertEquals(cfg1.getToken_endpoint(), "token-endpoint");
+        assertEquals(cfg1.getIntrospection_endpoint(), "introspect-endpoint");
+        assertEquals(cfg1.getIssuer(), "issuer");
+        assertEquals(cfg1.getJwks_uri(), "jwks-uri");
         assertEquals(Collections.singletonList("grant"), cfg1.getGrant_types_supported());
         assertEquals(Collections.singletonList("RS256"), cfg1.getToken_endpoint_auth_signing_alg_values_supported());
         assertEquals(Collections.singletonList("token"), cfg1.getResponse_types_supported());
@@ -66,6 +69,12 @@ public class OAuthConfigTest {
         assertNotEquals(cfg1, cfg2);
         cfg2.setToken_endpoint("token-endpoint");
 
+        cfg2.setIntrospection_endpoint("introspect-endpoint2");
+        assertNotEquals(cfg1, cfg2);
+        cfg2.setIntrospection_endpoint(null);
+        assertNotEquals(cfg1, cfg2);
+        cfg2.setIntrospection_endpoint("introspect-endpoint");
+
         cfg2.setAuthorization_endpoint("authz-endpoint2");
         assertNotEquals(cfg1, cfg2);
         cfg2.setAuthorization_endpoint(null);
diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/KeyStoreTest.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/KeyStoreTest.java
new file mode 100644
index 000000000..120d0c7b9
--- /dev/null
+++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/KeyStoreTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yahoo.athenz.auth;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+public class KeyStoreTest {
+
+    @Test
+    public void testKeyStore() {
+        KeyStore keyStore = (domain, service, keyId) -> "public-key";
+
+        assertEquals("public-key", keyStore.getPublicKey("domain", "service", "key1"));
+        assertNull(keyStore.getServicePublicKey("domain", "service", "key1"));
+    }
+}
diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/KeyStoreMock.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/KeyStoreMock.java
index 15be9604e..0d71e11cc 100644
--- a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/KeyStoreMock.java
+++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/KeyStoreMock.java
@@ -19,8 +19,10 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.security.PublicKey;
 
 import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.util.Crypto;
 
 public class KeyStoreMock implements KeyStore {
 
@@ -83,4 +85,10 @@ public class KeyStoreMock implements KeyStore {
 
         return null;
     }
+
+    @Override
+    public PublicKey getServicePublicKey(String domain, String service, String keyId) {
+        String publicKey = getPublicKey(domain, service, keyId);
+        return (publicKey == null) ? null : Crypto.loadPublicKey(publicKey);
+    }
 }
diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthorityTest.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthorityTest.java
new file mode 100644
index 000000000..5b72302b6
--- /dev/null
+++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/impl/ServiceAccessTokenAuthorityTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yahoo.athenz.auth.impl;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.Principal;
+import com.yahoo.athenz.auth.token.OAuth2Token;
+import com.yahoo.athenz.auth.util.Crypto;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.time.Instant;
+import java.util.Date;
+
+import static org.testng.Assert.*;
+
+public class ServiceAccessTokenAuthorityTest {
+
+    private final File ecPrivateKey = new File("./src/test/resources/unit_test_ec_private.key");
+    private final File ecPublicKey = new File("./src/test/resources/ec_public.key");
+
+    @Test
+    public void testServiceAccessTokenAuthority() {
+        ServiceAccessTokenAuthority serviceAccessTokenAuthority = new ServiceAccessTokenAuthority();
+        assertNotNull(serviceAccessTokenAuthority);
+        serviceAccessTokenAuthority.initialize();
+        assertEquals(serviceAccessTokenAuthority.getID(), "Auth-SvcAccessToken");
+        assertNull(serviceAccessTokenAuthority.getDomain());
+        assertEquals(serviceAccessTokenAuthority.getHeader(), "Authorization");
+        assertEquals(serviceAccessTokenAuthority.getAuthenticateChallenge(), "Basic realm=\"athenz\"");
+    }
+
+    @Test
+    public void testServiceAccessTokenAuthenticateFailures() {
+        ServiceAccessTokenAuthority serviceAccessTokenAuthority = new ServiceAccessTokenAuthority();
+        StringBuilder errMsg = new StringBuilder(512);
+        assertNull(serviceAccessTokenAuthority.authenticate(null, null, null, errMsg));
+        assertNotNull(errMsg);
+        assertTrue(errMsg.toString().contains("No credentials provided"));
+        errMsg.setLength(0);
+        assertNull(serviceAccessTokenAuthority.authenticate("", null, null, errMsg));
+        assertNotNull(errMsg);
+        assertTrue(errMsg.toString().contains("No credentials provided"));
+        errMsg.setLength(0);
+        assertNull(serviceAccessTokenAuthority.authenticate("Token", null, null, errMsg));
+        assertTrue(errMsg.toString().contains("No Bearer prefix"));
+        errMsg.setLength(0);
+        assertNull(serviceAccessTokenAuthority.authenticate("BearerToken", null, null, errMsg));
+        assertNotNull(errMsg);
+        assertTrue(errMsg.toString().contains("No Bearer prefix"));
+        errMsg.setLength(0);
+        assertNull(serviceAccessTokenAuthority.authenticate("Bearer Invalid-Token", null, null, errMsg));
+        assertNotNull(errMsg);
+        assertTrue(errMsg.toString().contains("Invalid token: exc=Unable to parse token"));
+    }
+
+    @Test
+    public void testServiceAccessTokenAuthenticate() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("https://athenz.io")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        ServiceAccessTokenAuthority serviceAccessTokenAuthority = new ServiceAccessTokenAuthority();
+        serviceAccessTokenAuthority.setKeyStore(new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domain, String service, String keyId) {
+                if ("athenz".equals(domain) && "api".equals(service) && "eckey1".equals(keyId)) {
+                    return Crypto.loadPublicKey(ecPublicKey);
+                } else {
+                    return null;
+                }
+            }
+        });
+        serviceAccessTokenAuthority.initialize();
+        StringBuilder errMsg = new StringBuilder(512);
+        Principal principal = serviceAccessTokenAuthority.authenticate("Bearer " + token, null, null, errMsg);
+        assertNotNull(principal);
+        assertEquals(principal.getDomain(), "athenz");
+        assertEquals(principal.getName(), "api");
+        assertEquals(principal.getCredentials(), "Bearer " + token);
+        assertEquals(principal.getIssueTime(), now);
+
+    }
+}
diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java
index e2b2cb05c..2e65b8713 100644
--- a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java
+++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/OAuth2TokenTest.java
@@ -24,6 +24,7 @@ import com.nimbusds.jose.util.Base64URL;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
+import com.yahoo.athenz.auth.KeyStore;
 import com.yahoo.athenz.auth.token.jwts.JwtsHelper;
 import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver;
 import com.yahoo.athenz.auth.util.Crypto;
@@ -229,7 +230,7 @@ public class OAuth2TokenTest {
                 .subject("subject")
                 .jwtID("id001")
                 .issueTime(Date.from(Instant.ofEpochSecond(now)))
-                .expirationTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
                 .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
                 .issuer("issuer")
                 .audience("audience")
@@ -314,4 +315,167 @@ public class OAuth2TokenTest {
         assertEquals(JwtsHelper.getLongClaim(claimsSet, "long", -1), -1);
         assertNull(JwtsHelper.getAudience(claimsSet));
     }
+
+    @Test
+    public void testOauth2TokenWithPublicKeyProvider() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("https://athenz.io")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = getKeyStore();
+        OAuth2Token oAuth2Token = new OAuth2Token(token, publicKeyProvider, "https://athenz.io");
+
+        assertEquals(oAuth2Token.getVersion(), 1);
+        assertEquals(oAuth2Token.getAudience(), "https://athenz.io");
+        assertEquals(oAuth2Token.getSubject(), "athenz.api");
+        assertEquals(oAuth2Token.getIssueTime(), now);
+        assertEquals(oAuth2Token.getExpiryTime(), now + 3600);
+        assertEquals(oAuth2Token.getNotBeforeTime(), now);
+        assertEquals(oAuth2Token.getAuthTime(), now);
+        assertEquals(oAuth2Token.getJwtId(), "id001");
+        assertEquals(oAuth2Token.getClientIdDomainName(), "athenz");
+        assertEquals(oAuth2Token.getClientIdServiceName(), "api");
+    }
+
+    KeyStore getKeyStore() {
+        return new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domainName, String serviceName, String keyId) {
+                if (domainName.equals("athenz") && serviceName.equals("api") && keyId.equals("eckey1")) {
+                    try {
+                        return Crypto.loadPublicKey(ecPublicKey);
+                    } catch (CryptoException e) {
+                        return null;
+                    }
+                }
+                return null;
+            }
+        };
+    }
+
+    String getSignedToken(final String issuer, final String subject, final String audience, final String keyId) throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject(subject)
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer(issuer)
+                .audience(audience)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(keyId).build(), claimsSet);
+        signedJWT.sign(signer);
+        return signedJWT.serialize();
+    }
+
+    void verifyTokenError(KeyStore publicKeyProvider, final String token, final String audience,
+                          final String expectedError) {
+        try {
+            new OAuth2Token(token, publicKeyProvider, audience);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains(expectedError));
+        }
+    }
+
+    @Test
+    public void testOauth2TokenWithPublicKeyProviderFailures() throws JOSEException {
+
+        KeyStore publicKeyProvider = getKeyStore();
+
+        // missing issuer
+
+        String token = getSignedToken(null, "athenz.api", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched issuer and subject");
+
+        // empty issuer
+
+        token = getSignedToken("", "athenz.api", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched issuer and subject");
+
+        // missing subject
+
+        token = getSignedToken("athenz.api", null, "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched issuer and subject");
+
+        // empty subject
+
+        token = getSignedToken("athenz.api", "", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched issuer and subject");
+
+        // mismatched issuer and subject
+
+        token = getSignedToken("athenz.api", "athenz.backend", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched issuer and subject");
+
+        // missing audience
+
+        token = getSignedToken("athenz.api", "athenz.api", null, "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched audience");
+
+        // empty audience
+
+        token = getSignedToken("athenz.api", "athenz.api", "", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing or mismatched audience");
+
+        // mismatched audience
+
+        token = getSignedToken("athenz.api", "athenz.api", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://token.athenz.io", "Invalid token: missing or mismatched audience");
+
+        // invalid service identifier
+
+        token = getSignedToken("athenz", "athenz", "https://athenz.io", "eckey1");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing domain and service");
+
+        // missing key id
+
+        token = getSignedToken("athenz.api", "athenz.api", "https://athenz.io", null);
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing key id");
+
+        // empty key id
+
+        token = getSignedToken("athenz.api", "athenz.api", "https://athenz.io", "");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: missing key id");
+
+        // unknown public key id
+
+        token = getSignedToken("athenz.api", "athenz.api", "https://athenz.io", "eckey2");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Invalid token: unable to get public key");
+
+        // invalid signature
+
+        token = getSignedToken("athenz.api", "athenz.api", "https://athenz.io", "eckey1");
+        token = token.substring(0, token.lastIndexOf('.') + 1) + Base64URL.encode("invalid-signature");
+        verifyTokenError(publicKeyProvider, token, "https://athenz.io", "Unable to verify token signature");
+
+        // invalid token
+
+        verifyTokenError(publicKeyProvider, "invalid-token", "https://athenz.io", "Unable to parse token");
+    }
 }
diff --git a/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/ZTSAccessTokenTest.java b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/ZTSAccessTokenTest.java
new file mode 100644
index 000000000..bf1befd1b
--- /dev/null
+++ b/libs/java/auth_core/src/test/java/com/yahoo/athenz/auth/token/ZTSAccessTokenTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yahoo.athenz.auth.token;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.yahoo.athenz.auth.Authorizer;
+import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.Principal;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.athenz.auth.util.CryptoException;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.time.Instant;
+import java.util.Date;
+
+import static org.testng.Assert.*;
+
+public class ZTSAccessTokenTest {
+
+    private final File ecPrivateKey = new File("./src/test/resources/unit_test_ec_private.key");
+    private final File ecPublicKey = new File("./src/test/resources/ec_public.key");
+
+    @Test
+    public void testZTSAccessToken() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("sports")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domainName, String serviceName, String keyId) {
+                if (domainName.equals("sys.auth") && serviceName.equals("zts") && keyId.equals("eckey1")) {
+                    try {
+                        return Crypto.loadPublicKey(ecPublicKey);
+                    } catch (CryptoException ignored) {
+                    }
+                }
+                return null;
+            }
+        };
+
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        Principal principal = Mockito.mock(Principal.class);
+        Mockito.when(authorizer.access("introspect", "sports:token", principal, null))
+                .thenReturn(true) // authorize our introspect request
+                .thenReturn(false); // unauthorized introspect request
+
+        ZTSAccessToken ztsAccessToken = new ZTSAccessToken(token, publicKeyProvider, authorizer, principal);
+
+        assertEquals(ztsAccessToken.getVersion(), 1);
+        assertEquals(ztsAccessToken.getAudience(), "sports");
+        assertEquals(ztsAccessToken.getSubject(), "athenz.api");
+        assertEquals(ztsAccessToken.getIssueTime(), now);
+        assertEquals(ztsAccessToken.getExpiryTime(), now + 3600);
+        assertEquals(ztsAccessToken.getNotBeforeTime(), now);
+        assertEquals(ztsAccessToken.getAuthTime(), now);
+        assertEquals(ztsAccessToken.getJwtId(), "id001");
+
+        // during our second call we should get a failure from the authorizer
+
+        try {
+            new ZTSAccessToken(token, publicKeyProvider, authorizer, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("unauthorized introspect request"));
+        }
+    }
+
+    @Test
+    public void testZTSAccessTokenFailuresNullArguments() {
+
+        try {
+            new ZTSAccessToken(null, null, null, null);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid arguments: missing token, public key provider, authorizer or principal"));
+        }
+
+        try {
+            new ZTSAccessToken("token", null, null, null);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid arguments: missing token, public key provider, authorizer or principal"));
+        }
+
+        KeyStore publicKeyProvider = new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domainName, String serviceName, String keyId) {
+                return null;
+            }
+        };
+        try {
+            new ZTSAccessToken("token", publicKeyProvider, null, null);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid arguments: missing token, public key provider, authorizer or principal"));
+        }
+
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        try {
+            new ZTSAccessToken("token", publicKeyProvider, authorizer, null);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid arguments: missing token, public key provider, authorizer or principal"));
+        }
+
+        Principal principal = Mockito.mock(Principal.class);
+        try {
+            new ZTSAccessToken("token", publicKeyProvider, null, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid arguments: missing token, public key provider, authorizer or principal"));
+        }
+    }
+
+    @Test
+    public void testZTSAccessTokenWithoutAudience() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = Mockito.mock(KeyStore.class);
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        Principal principal = Mockito.mock(Principal.class);
+
+        try {
+            new ZTSAccessToken(token, publicKeyProvider, authorizer, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid token: missing audience"));
+        }
+    }
+
+    @Test
+    public void testZTSAccessTokenWithoutKeyId() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("sports")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = Mockito.mock(KeyStore.class);
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        Principal principal = Mockito.mock(Principal.class);
+        Mockito.when(authorizer.access("introspect", "sports:token", principal, null))
+                .thenReturn(true);
+
+        try {
+            new ZTSAccessToken(token, publicKeyProvider, authorizer, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid token: missing key id"));
+        }
+    }
+
+    @Test
+    public void testZTSAccessTokenMissingPublicKey() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("sports")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domainName, String serviceName, String keyId) {
+                return null;
+            }
+        };
+
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        Principal principal = Mockito.mock(Principal.class);
+        Mockito.when(authorizer.access("introspect", "sports:token", principal, null))
+                .thenReturn(true);
+
+        try {
+            new ZTSAccessToken(token, publicKeyProvider, authorizer, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Invalid token: unable to get public key"));
+        }
+    }
+
+    @Test
+    public void testZTSAccessTokenInvalidSignature() throws JOSEException {
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .jwtID("id001")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .notBeforeTime(Date.from(Instant.ofEpochSecond(now)))
+                .issuer("athenz.api")
+                .audience("sports")
+                .claim(OAuth2Token.CLAIM_AUTH_TIME, now)
+                .claim(OAuth2Token.CLAIM_VERSION, 1)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = new KeyStore() {
+            @Override
+            public String getPublicKey(String domain, String service, String keyId) {
+                return null;
+            }
+
+            @Override
+            public PublicKey getServicePublicKey(String domainName, String serviceName, String keyId) {
+                if (domainName.equals("sys.auth") && serviceName.equals("zts") && keyId.equals("eckey1")) {
+                    try {
+                        return Crypto.loadPublicKey(ecPublicKey);
+                    } catch (CryptoException ignored) {
+                    }
+                }
+                return null;
+            }
+        };
+
+        Authorizer authorizer = Mockito.mock(Authorizer.class);
+        Principal principal = Mockito.mock(Principal.class);
+        Mockito.when(authorizer.access("introspect", "sports:token", principal, null))
+                .thenReturn(true) // authorize our introspect request
+                .thenReturn(false); // unauthorized introspect request
+
+        final String unsignedToken = token.substring(0, token.lastIndexOf('.') + 1);
+        final String signedToken = unsignedToken + Base64URL.encode("invalid-signature");
+
+        try {
+            new ZTSAccessToken(signedToken, publicKeyProvider, authorizer, principal);
+            fail();
+        } catch (CryptoException ex) {
+            assertTrue(ex.getMessage().contains("Unable to verify token signature"));
+        }
+    }
+}
diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommonTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommonTest.java
index 1d3731974..85ae2aec5 100644
--- a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommonTest.java
+++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/notification/NotificationConverterCommonTest.java
@@ -239,11 +239,35 @@ public class NotificationConverterCommonTest {
     }
 
     @Test
-    public void testGetWorkflowUrl() {
+    public void testGetAdminWorkflowUrl() {
         System.setProperty("athenz.notification_workflow_url", "https://athenz.io");
         NotificationConverterCommon converter = new NotificationConverterCommon(null);
-        assertEquals(converter.getWorkflowUrl(), "https://athenz.io");
+        assertEquals(converter.getAdminWorkflowUrl(), "https://athenz.io");
+
         System.clearProperty("athenz.notification_workflow_url");
+        converter = new NotificationConverterCommon(null);
+        // if workflow url is not set, then we should return empty string
+        assertEquals(converter.getAdminWorkflowUrl(), "");
+    }
+
+    @Test
+    public void testGetDominWorkflowUrl() {
+        NotificationConverterCommon converter = new NotificationConverterCommon(null);
+        // if Athenz UI url is not set, then we should return empty string
+        assertEquals(converter.getDomainWorkflowUrl(null), "");
+
+        // set Athenz UI url
+        System.setProperty("athenz.notification_athenz_ui_url", "https://athenz.io");
+        converter = new NotificationConverterCommon(null);
+
+        // if domainName is empty or null, return domain workflow url
+        assertEquals(converter.getDomainWorkflowUrl(""), "https://athenz.io/workflow/domain");
+        assertEquals(converter.getDomainWorkflowUrl(null), "https://athenz.io/workflow/domain");
+
+        // if domainName is set, return domain workflow url with query param set for domainName
+        assertEquals(converter.getDomainWorkflowUrl("test.dom1"), "https://athenz.io/workflow/domain?domain=test.dom1");
+
+        System.clearProperty("athenz.notification_athenz_ui_url");
     }
 
     @Test
diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/rest/ResourceContextTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/rest/ResourceContextTest.java
index 3e6c43620..081978abc 100644
--- a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/rest/ResourceContextTest.java
+++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/rest/ResourceContextTest.java
@@ -50,6 +50,10 @@ public class ResourceContextTest {
         assertEquals(context.servletContext(), servletContext);
         assertNull(context.principal());
 
+        Principal principal = Mockito.mock(Principal.class);
+        context.setPrincipal(principal);
+        assertEquals(context.principal(), principal);
+
         context.setRequestDomain("request-domain");
         assertEquals(context.getRequestDomain(), "request-domain");
     }
diff --git a/libs/java/server_notification_slack/src/test/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClientTest.java b/libs/java/server_notification_slack/src/test/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClientTest.java
index 6796cc7d1..29712c445 100644
--- a/libs/java/server_notification_slack/src/test/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClientTest.java
+++ b/libs/java/server_notification_slack/src/test/java/com/yahoo/athenz/common/notification/slack/client/AthenzSlackClientTest.java
@@ -254,7 +254,7 @@ public class AthenzSlackClientTest {
         boolean result = slackClient.sendMessage(
                 Collections.singleton("invalid@example.com"), "test message");
         assertFalse(result);
-        verify(mockMethods, times(1)).usersLookupByEmail((RequestConfigurator<UsersLookupByEmailRequest.UsersLookupByEmailRequestBuilder>) any());
+        verify(mockMethods, times(4)).usersLookupByEmail((RequestConfigurator<UsersLookupByEmailRequest.UsersLookupByEmailRequestBuilder>) any());
         verify(mockMethods, never()).chatPostMessage(any(ChatPostMessageRequest.class));
     }
 
@@ -337,7 +337,7 @@ public class AthenzSlackClientTest {
 
         boolean result = slackClient.sendMessage(Collections.singleton("test-channel"), "test message");
         assertFalse(result);
-        verify(mockMethods, times(1)).chatPostMessage(any(ChatPostMessageRequest.class));
+        verify(mockMethods, times(4)).chatPostMessage(any(ChatPostMessageRequest.class));
 
         assertTrue(Thread.interrupted()); // clears interrupted status
     }
@@ -383,7 +383,7 @@ public class AthenzSlackClientTest {
         boolean result = slackClient.sendMessage(
                 Collections.singleton("user@example.com"), "test message");
         assertFalse(result);
-        verify(mockMethods, times(1)).usersLookupByEmail((RequestConfigurator<UsersLookupByEmailRequest.UsersLookupByEmailRequestBuilder>) any());
+        verify(mockMethods, times(4)).usersLookupByEmail((RequestConfigurator<UsersLookupByEmailRequest.UsersLookupByEmailRequestBuilder>) any());
         verify(mockMethods, never()).chatPostMessage(any(ChatPostMessageRequest.class));
 
         // Clear the interrupted status for other tests
diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
index bec3e8dc3..8d210d541 100644
--- a/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
+++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ZMSImplTest.java
@@ -75,6 +75,7 @@ import java.lang.reflect.Field;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.interfaces.RSAPublicKey;
 import java.text.DateFormat;
 import java.text.ParseException;
@@ -9960,14 +9961,17 @@ public class ZMSImplTest {
         String publicKey = zmsImpl.getPublicKey("sys.auth", "zms", "0");
         assertNotNull(publicKey);
         assertEquals(zmsTestInitializer.getPubKey(), Crypto.ybase64(publicKey.getBytes(StandardCharsets.UTF_8)));
+        assertNotNull(zmsImpl.getServicePublicKey("sys.auth", "zms", "0"));
 
         publicKey = zmsImpl.getPublicKey("sys.auth", "zms", "1");
         assertNotNull(publicKey);
         assertEquals(zmsTestInitializer.getPubKeyK1(), Crypto.ybase64(publicKey.getBytes(StandardCharsets.UTF_8)));
+        assertNotNull(zmsImpl.getServicePublicKey("sys.auth", "zms", "1"));
 
         publicKey = zmsImpl.getPublicKey("sys.auth", "zms", "2");
         assertNotNull(publicKey);
         assertEquals(zmsTestInitializer.getPubKeyK2(), Crypto.ybase64(publicKey.getBytes(StandardCharsets.UTF_8)));
+        assertNotNull(zmsImpl.getServicePublicKey("sys.auth", "zms", "2"));
     }
 
     @Test
@@ -9975,8 +9979,10 @@ public class ZMSImplTest {
 
         ZMSImpl zmsImpl = zmsTestInitializer.getZms();
 
-        String pubKey = zmsImpl.getPublicKey("sys.auth", "sys.auth", "0");
-        assertNull(pubKey);
+        assertNull(zmsImpl.getPublicKey("sys.auth", "sys.auth", "0"));
+        assertNull(zmsImpl.getServicePublicKey("sys.auth", "sys.auth", "0"));
+        assertNull(zmsImpl.getServicePublicKey("sys.auth", null, "0"));
+        assertNull(zmsImpl.getServicePublicKey("sys.auth", "zms", null));
     }
 
     @Test
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/RsrcCtxWrapperTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/RsrcCtxWrapperTest.java
index 780d957c1..9d8760caf 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/RsrcCtxWrapperTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/RsrcCtxWrapperTest.java
@@ -81,6 +81,11 @@ public class RsrcCtxWrapperTest {
 
         // after authenticate, principal should be set
         assertEquals(wrapper.principal(), prin);
+
+        // update the principal and verify the new value
+        Principal newPrincipal = Mockito.mock(Principal.class);
+        wrapper.setPrincipal(newPrincipal);
+        assertEquals(wrapper.principal(), newPrincipal);
     }
 
     @Test
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java
index ecc38ad87..c40f68caa 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSImplTest.java
@@ -15,9 +15,8 @@
  */
 package com.yahoo.athenz.zts;
 
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSObject;
-import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.*;
+import com.nimbusds.jose.crypto.ECDSASigner;
 import com.nimbusds.jose.crypto.RSASSAVerifier;
 import com.nimbusds.jose.util.Base64URL;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -74,8 +73,8 @@ import com.yahoo.athenz.zts.store.CloudStore;
 import com.yahoo.athenz.zts.store.DataStore;
 import com.yahoo.athenz.zts.store.MockCloudStore;
 import com.yahoo.athenz.zts.store.MockZMSFileChangeLogStore;
-import com.yahoo.athenz.zts.token.AccessTokenRequest;
-import com.yahoo.athenz.zts.token.IdTokenRequest;
+import com.yahoo.athenz.zts.token.AccessTokenScope;
+import com.yahoo.athenz.zts.token.IdTokenScope;
 import com.yahoo.athenz.zts.utils.ZTSUtils;
 import com.yahoo.rdl.Schema;
 import com.yahoo.rdl.Struct;
@@ -106,8 +105,10 @@ import java.nio.file.Paths;
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.cert.X509Certificate;
+import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
@@ -141,6 +142,10 @@ public class ZTSImplTest {
             + "EtFVWZTU2dwWHIzQ3pkaDFhMjZkbGI3bW1LMjlxbVhKWGg2dW1XOUF5ZlRPS1ZvCis2QVNsb1ZVM2F2dnVmbE"
             + "dVT0VnMmpzbWRha1IyNEtjTGpBdTZRclVlNDE3bEczdDhxU1BJR2pTNUMrQ3NKVXcKaDA0aEh4NWYrUEV3eFY"
             + "0cmJRSURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo-";
+    private static final String ZTS_Y64_PUB_EC = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FR"
+            + "WUlLb1pJemowREFRY0RRZ0FFalRIckFSU1RsUFNHeVZwUHpjTTFYTG12M3hlYwpic2NDTkRLZU1LdHgwSjRCT"
+            + "jFYWjV1bDUrb0dXTDlKZG5DOHZmN3M2SVBjeE92SVp0SDdORklWbit3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS"
+            + "0tLS0tCg--";
     private static final String ZTS_PEM_CERT0 = "-----BEGIN PUBLIC KEY-----\n"
             + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1tGSVCA8wl5ew5Y76Wj2rJAUD\n"
             + "YanEJfKmAlx5cQ/8hKEUfSSgpXr3Czdh1a26dlb7mmK29qmXJXh6umW9AyfTOKVo\n"
@@ -235,7 +240,6 @@ public class ZTSImplTest {
                 privateKey, "0");
 
         cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
 
         System.setProperty(ZTSConsts.ZTS_PROP_CERT_ALLOWED_O_VALUES, "Athenz, Inc.|My Test Company|Athenz|Yahoo");
         System.setProperty(ZTSConsts.ZTS_PROP_NOAUTH_URI_LIST, "/zts/v1/schema,/zts/v1/status");
@@ -247,6 +251,7 @@ public class ZTSImplTest {
         ZTSTestUtils.deleteDirectory(new File("/tmp/zts_server_cert_store"));
         System.setProperty(ZTSConsts.ZTS_PROP_CERT_FILE_STORE_PATH, "/tmp/zts_server_cert_store");
         System.setProperty(ZTSConsts.ZTS_PROP_VALIDATE_SERVICE_IDENTITY, "false");
+        System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.cloud:4443/zts/v1");
 
         // enable ip validation for cert requests
 
@@ -260,7 +265,7 @@ public class ZTSImplTest {
 
         // enable openid scope
 
-        AccessTokenRequest.setSupportOpenIdScope(true);
+        AccessTokenScope.setSupportOpenIdScope(true);
     }
 
     @AfterMethod
@@ -358,6 +363,7 @@ public class ZTSImplTest {
         List<RoleMember> writers = new ArrayList<>();
         writers.add(new RoleMember().setMemberName("user_domain.user"));
         writers.add(new RoleMember().setMemberName("user_domain.user1"));
+        writers.add(new RoleMember().setMemberName("coretech.jwt"));
 
         List<RoleMember> readers = new ArrayList<>();
         readers.add(new RoleMember().setMemberName("user_domain.user3"));
@@ -526,6 +532,12 @@ public class ZTSImplTest {
         service.setHosts(hosts);
         service.setModified(Timestamp.fromCurrentTime());
         services.add(service);
+
+        service = new ServiceIdentity();
+        service.setName(generateServiceIdentityName(domainName, "jwt"));
+        setServicePublicKey(service, "0", ZTS_Y64_PUB_EC);
+        services.add(service);
+
         return services;
     }
 
@@ -1022,13 +1034,13 @@ public class ZTSImplTest {
         String domain = "unknown";
         String service = "unknown";
 
-        String pubKey = zts.getPublicKey(domain, service, "0");
+        String pubKey = zts.dataStore.getPublicKey(domain, service, "0");
         assertNull(pubKey);
 
-        pubKey = zts.getPublicKey(null, service, "0");
+        pubKey = zts.dataStore.getPublicKey(null, service, "0");
         assertNull(pubKey);
 
-        pubKey = zts.getPublicKey(domain, null, "0");
+        pubKey = zts.dataStore.getPublicKey(domain, null, "0");
         assertNull(pubKey);
     }
 
@@ -1038,10 +1050,10 @@ public class ZTSImplTest {
         SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true);
         store.processSignedDomain(signedDomain, false);
 
-        String pubKey = zts.getPublicKey("coretech", "storage", "0");
+        String pubKey = zts.dataStore.getPublicKey("coretech", "storage", "0");
         assertEquals(pubKey, ZTS_PEM_CERT0);
 
-        pubKey = zts.getPublicKey("coretech", "storage", "100");
+        pubKey = zts.dataStore.getPublicKey("coretech", "storage", "100");
         assertNull(pubKey);
     }
 
@@ -2468,9 +2480,10 @@ public class ZTSImplTest {
         ResourceContext context = createResourceContext(principal);
 
         com.yahoo.athenz.zts.ServiceIdentityList svcList = zts.getServiceIdentityList(context, "coretech");
-        assertEquals(svcList.getNames().size(), 2);
+        assertEquals(svcList.getNames().size(), 3);
         assertTrue(svcList.getNames().contains("storage"));
         assertTrue(svcList.getNames().contains("backup"));
+        assertTrue(svcList.getNames().contains("jwt"));
     }
 
     @Test
@@ -9846,7 +9859,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -9929,7 +9941,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -9972,7 +9983,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10034,7 +10044,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private_ec.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10086,7 +10095,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10110,7 +10118,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10182,7 +10189,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10242,7 +10248,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10312,12 +10317,11 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
 
-        AccessTokenRequest.setSupportOpenIdScope(false);
+        AccessTokenScope.setSupportOpenIdScope(false);
 
         SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true);
         store.processSignedDomain(signedDomain, false);
@@ -10459,7 +10463,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10518,7 +10521,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -10638,7 +10640,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -11590,18 +11591,6 @@ public class ZTSImplTest {
         assertEquals(zts.getInstanceRegisterQueryLog("aws", "id001", hostnameBuilder + "01234"), check);
     }
 
-    @Test
-    public void testGetQueryLogData() {
-
-        String request = "data\ntest\ragain";
-        assertEquals(zts.getQueryLogData(request), "data_test_again");
-
-        // generate a string with 1024 length
-
-        request = "0123456789012345".repeat(64);
-        assertEquals(zts.getQueryLogData(request + "abcd"), request);
-    }
-
     @Test
     public void testGenerateSSHCertRecord() {
 
@@ -12199,7 +12188,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -12250,7 +12238,6 @@ public class ZTSImplTest {
         System.setProperty(ZTSConsts.ZTS_PROP_SYSTEM_AUTHZ_DETAILS_PATH, "src/test/resources/system_single_authz_details.json");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
 
         // set back to our zts rsa private key and clear authz details path
@@ -12358,7 +12345,6 @@ public class ZTSImplTest {
         System.setProperty(ZTSConsts.ZTS_PROP_SYSTEM_AUTHZ_DETAILS_PATH, "src/test/resources/system_single_authz_details.json");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
 
         // set back to our zts rsa private key and clear authz details path
@@ -12459,7 +12445,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -12557,7 +12542,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -13568,6 +13552,9 @@ public class ZTSImplTest {
         assertEquals(oauthConfig.getAuthorization_endpoint(), "https://athenz.cloud:4443/zts/v1/oauth2/auth");
         assertEquals(oauthConfig.getToken_endpoint(), "https://athenz.cloud:4443/zts/v1/oauth2/token");
 
+        // by default introspection is not enabled
+        assertNull(oauthConfig.getIntrospection_endpoint());
+
         assertEquals(Collections.singletonList("RS256"), oauthConfig.getToken_endpoint_auth_signing_alg_values_supported());
 
         List<String> supportedTypes = oauthConfig.getResponse_types_supported();
@@ -13576,6 +13563,19 @@ public class ZTSImplTest {
         assertTrue(supportedTypes.contains("id_token token"));
 
         assertEquals(Collections.singletonList("client_credentials"), oauthConfig.getGrant_types_supported());
+
+        // now let's enable introspection
+
+        System.setProperty(ZTSConsts.ZTS_PROP_INTROSPECT_SUPPORT_ENABLED, "true");
+        ZTSImpl ztsImpl = new ZTSImpl(mockCloudStore, store);
+        oauthConfig = ztsImpl.getOAuthConfig(ctx);
+        assertNotNull(oauthConfig);
+        assertEquals(oauthConfig.getIssuer(), "https://athenz.cloud:4443/zts/v1");
+        assertEquals(oauthConfig.getJwks_uri(), "https://athenz.cloud:4443/zts/v1/oauth2/keys?rfc=true");
+        assertEquals(oauthConfig.getAuthorization_endpoint(), "https://athenz.cloud:4443/zts/v1/oauth2/auth");
+        assertEquals(oauthConfig.getToken_endpoint(), "https://athenz.cloud:4443/zts/v1/oauth2/token");
+        assertEquals(oauthConfig.getIntrospection_endpoint(), "https://athenz.cloud:4443/zts/v1/oauth2/introspect");
+        System.clearProperty(ZTSConsts.ZTS_PROP_INTROSPECT_SUPPORT_ENABLED);
     }
 
     @Test
@@ -13732,7 +13732,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         ztsImpl.oidcPort = 443;
         ztsImpl.ztsOIDCPortIssuer = "https://athenz.io";
@@ -13770,7 +13769,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -13891,7 +13889,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -13962,10 +13959,9 @@ public class ZTSImplTest {
 
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
-        IdTokenRequest.setMaxDomains(10);
+        IdTokenScope.setMaxDomains(10);
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -14155,7 +14151,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         ztsImpl.userDomain = "user_domain";
 
@@ -14321,7 +14316,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -14373,10 +14367,9 @@ public class ZTSImplTest {
 
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
-        IdTokenRequest.setMaxDomains(10);
+        IdTokenScope.setMaxDomains(10);
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         // set back to our zts rsa private key
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
@@ -14599,7 +14592,6 @@ public class ZTSImplTest {
     public void testGetInfo() throws URISyntaxException, FileNotFoundException {
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         ztsImpl.serverInfo = null;
 
@@ -14639,7 +14631,6 @@ public class ZTSImplTest {
     public void testGetInfoException() throws URISyntaxException, FileNotFoundException {
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         ztsImpl.serverInfo = null;
 
@@ -15020,7 +15011,6 @@ public class ZTSImplTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
 
         CloudStore cloudStore = new CloudStore();
-        //cloudStore.setHttpClient(null);
         ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
         ztsImpl.userDomain = "user_domain";
 
@@ -15209,4 +15199,185 @@ public class ZTSImplTest {
         System.clearProperty("athenz.instance.test.provider.scheme");
         System.clearProperty("athenz.instance.test.provider.close.exception");
     }
+
+    @Test
+    public void testPostAccessTokenRequestWithJWTBearerToken() throws JOSEException {
+
+        System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
+        System.setProperty(ZTSConsts.ZTS_PROP_SYSTEM_AUTHZ_DETAILS_PATH, "src/test/resources/system_single_authz_details.json");
+
+        CloudStore cloudStore = new CloudStore();
+        ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
+
+        // set back to our zts rsa private key and clear authz details path
+
+        System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
+        System.clearProperty(ZTSConsts.ZTS_PROP_SYSTEM_AUTHZ_DETAILS_PATH);
+
+        SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true);
+        store.processSignedDomain(signedDomain, false);
+
+        HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
+        Mockito.when(servletRequest.isSecure()).thenReturn(true);
+        ResourceContext context = new RsrcCtxWrapper(null, servletRequest, null, null, true,
+                null, null, null, "postaccesstoken");
+
+        // first let's try without any client assertions which should
+        // return the request at not authenticated
+
+        try {
+            ztsImpl.postAccessTokenRequest(context, "grant_type=client_credentials&scope=coretech:role.writers");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), ResourceException.UNAUTHORIZED);
+        }
+
+        // now let's generate our bearer token and try again
+        
+        long now = System.currentTimeMillis() / 1000;
+
+        File privateKeyFile = new File("src/test/resources/unit_test_zts_private_ec.pem");
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) Crypto.loadPrivateKey(privateKeyFile));
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("coretech.jwt")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .issuer("coretech.jwt")
+                .audience(zts.ztsOAuthIssuer)
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("0").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String jwtToken = signedJWT.serialize();
+
+        // first role based match
+
+        AccessTokenResponse resp = ztsImpl.postAccessTokenRequest(context,
+                "grant_type=client_credentials&scope=coretech:role.writers" +
+                "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" +
+                "&client_assertion=" + jwtToken);
+        assertNotNull(resp);
+        assertNull(resp.getScope());
+
+        ServerPrivateKey privateKey = getServerPrivateKey(ztsImpl, ztsImpl.keyAlgoForJsonWebObjects);
+        String accessTokenStr = resp.getAccess_token();
+        JWSVerifier verifier = JwtsHelper.getJWSVerifier(Crypto.extractPublicKey(privateKey.getKey()));
+        JWTClaimsSet claimSet = null;
+        try {
+            SignedJWT signedJWTRes = SignedJWT.parse(accessTokenStr);
+            assertTrue(signedJWTRes.verify(verifier));
+            claimSet = signedJWTRes.getJWTClaimsSet();
+        } catch(Exception ex) {
+            fail(ex.getMessage());
+        }
+        try {
+            assertNotNull(claimSet);
+            assertEquals(claimSet.getAudience().get(0), "coretech");
+            assertEquals(ztsImpl.ztsOAuthIssuer, claimSet.getIssuer());
+            List<String> scopes = claimSet.getStringListClaim("scp");
+            assertNotNull(scopes);
+            assertEquals(scopes.size(), 1);
+            assertEquals(scopes.get(0), "writers");
+        } catch (ParseException ex) {
+            fail(ex.getMessage());
+        }
+    }
+
+    @Test
+    public void testExtractTokenValue() {
+
+        assertNull(zts.extractTokenValue(null));
+        assertNull(zts.extractTokenValue(""));
+        assertNull(zts.extractTokenValue("key1=value1"));
+        assertNull(zts.extractTokenValue("key1"));
+        assertNull(zts.extractTokenValue("key1=value1&key2=value2"));
+        assertNull(zts.extractTokenValue("key1=value1&key2=value2&key3=value3"));
+
+        assertEquals(zts.extractTokenValue("key1=value1&token=1234"), "1234");
+        assertEquals(zts.extractTokenValue("key1=value1&token=1234&key3=value3"), "1234");
+        assertEquals(zts.extractTokenValue("token=1234&key3=value3"), "1234");
+    }
+
+    @Test
+    public void testPostIntrospectRequest() throws KeySourceException {
+
+        System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_at_private.pem");
+
+        CloudStore cloudStore = new CloudStore();
+        ZTSImpl ztsImpl = new ZTSImpl(cloudStore, store);
+        // set back to our zts rsa private key
+        System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY, "src/test/resources/unit_test_zts_private.pem");
+
+        SignedDomain signedDomain = createSignedDomain("coretech", "weather", "storage", true);
+        store.processSignedDomain(signedDomain, false);
+
+        Principal principal = SimplePrincipal.create("user_domain", "user",
+                "v=U1;d=user_domain;n=user;s=signature", 0, null);
+        ResourceContext context = createResourceContext(principal);
+
+        final String scope = URLEncoder.encode("coretech:domain", StandardCharsets.UTF_8);
+        AccessTokenResponse resp = ztsImpl.postAccessTokenRequest(context,
+                "grant_type=client_credentials&scope=" + scope);
+        assertNotNull(resp);
+
+        ServerPrivateKey privateKey = getServerPrivateKey(ztsImpl, ztsImpl.keyAlgoForJsonWebObjects);
+        PublicKey publicKey = Crypto.extractPublicKey(privateKey.getKey());
+
+        DataStore dataStore = Mockito.mock(DataStore.class);
+        Mockito.when(dataStore.getServicePublicKey("sys.auth", "zts", "0")).thenReturn(publicKey);
+        ztsImpl.dataStore = dataStore;
+
+        ZTSAuthorizer authorizer = Mockito.mock(ZTSAuthorizer.class);
+        Mockito.when(authorizer.access("introspect", "coretech:token", principal, null)).thenReturn(true);
+        ztsImpl.authorizer = authorizer;
+
+        String accessTokenStr = resp.getAccess_token();
+        final String request = "token=" + URLEncoder.encode(accessTokenStr, StandardCharsets.UTF_8);
+
+        // by default the feature is disabled
+
+        try {
+            ztsImpl.postIntrospectRequest(context, request);
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+            assertTrue(ex.getMessage().contains("Introspection is not supported"));
+        }
+
+        // now let's enable the feature
+
+        ztsImpl.introspectSupportEnabled = true;
+        IntrospectResponse intResp = ztsImpl.postIntrospectRequest(context, request);
+        assertNotNull(intResp);
+        assertTrue(intResp.getActive());
+        assertEquals(intResp.getScope(), "writers");
+        assertEquals(intResp.getAud(), "coretech");
+        assertEquals(intResp.getClient_id(), "user_domain.user");
+        assertEquals(intResp.getIss(), ztsImpl.ztsOAuthIssuer);
+        assertEquals(intResp.getSub(), "user_domain.user");
+        assertEquals(intResp.getUid(), "user_domain.user");
+
+        // with empty token we'll get request error
+
+        try {
+            ztsImpl.postIntrospectRequest(context, "key=value");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            ztsImpl.postIntrospectRequest(context, "token=");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        // invalid token returns inactive
+
+        intResp = ztsImpl.postIntrospectRequest(context, "token=invalid-token");
+        assertNotNull(intResp);
+        assertFalse(intResp.getActive());
+    }
+
 }
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSTest.java
index 6ffb6b665..c18f81987 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/ZTSTest.java
@@ -44,6 +44,7 @@ public class ZTSTest {
         System.setProperty(FilePrivateKeyStore.ATHENZ_PROP_PRIVATE_KEY,
             "src/test/resources/unit_test_zts_private.pem");
         System.setProperty(ZTSConsts.ZTS_PROP_WORKLOAD_FILE_STORE_PATH, "/tmp/zts_server_workloads_store");
+        System.setProperty(ZTSConsts.ZTS_PROP_OPENID_ISSUER, "https://athenz.cloud:4443/zts/v1");
     }
     
     @AfterClass
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/store/DataStoreTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/store/DataStoreTest.java
index 91974b2d2..657500e2a 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/store/DataStoreTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/store/DataStoreTest.java
@@ -1356,8 +1356,41 @@ public class DataStoreTest {
         store.addDomainToCache("coretech", dataCache);
         
         assertEquals(store.getPublicKey("coretech", "storage", "0"), ZTS_PEM_CERT0);
+        PublicKey publicKeyObj = store.getServicePublicKey("coretech", "storage", "0");
+        assertNotNull(publicKeyObj);
+        assertEquals(Crypto.loadPublicKey(ZTS_PEM_CERT0), publicKeyObj);
+
+        // remove the public key from the pem cache directly and verify that our
+        // public key cache still works fine
+
+        store.publicKeyCache.remove("coretech.storage_0");
+        assertNull(store.getPublicKey("coretech", "storage", "0"));
+        assertNotNull(store.getServicePublicKey("coretech", "storage", "0"));
+
         assertEquals(store.getPublicKey("coretech", "storage", "1"), ZTS_PEM_CERT1);
+        publicKeyObj = store.getServicePublicKey("coretech", "storage", "1");
+        assertNotNull(publicKeyObj);
+        assertEquals(Crypto.loadPublicKey(ZTS_PEM_CERT1), publicKeyObj);
+
+        // now remove the public key from the pem cache using the regular
+        // method which should also remove it from the svc public key cache
+
+        HashMap <String, String> removeKeyMap = new HashMap<>();
+        removeKeyMap.put("coretech.storage_1", ZTS_PEM_CERT1);
+        store.removePublicKeys(removeKeyMap);
+        assertNull(store.getPublicKey("coretech", "storage", "1"));
+        assertNull(store.getServicePublicKey("coretech", "storage", "1"));
+
+        // an unknown key id should always return null
+
         assertNull(store.getPublicKey("coretech", "storage", "2"));
+        assertNull(store.getServicePublicKey("coretech", "storage", "2"));
+
+        // let's also directly add an invalid public key to the pem cache
+
+        store.publicKeyCache.put("coretech.storage_3", "invalid-key");
+        assertNotNull(store.getPublicKey("coretech", "storage", "3"));
+        assertNull(store.getServicePublicKey("coretech", "storage", "3"));
     }
 
     @Test
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenBodyTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenBodyTest.java
deleted file mode 100644
index 2098af19e..000000000
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenBodyTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright The Athenz Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.yahoo.athenz.zts.token;
-
-import org.testng.annotations.Test;
-
-import static org.testng.Assert.*;
-
-public class AccessTokenBodyTest {
-
-    @Test
-    public void testAccessTokenBody() {
-
-        AccessTokenBody body = new AccessTokenBody("grant_type=client_credentials&scope=coretech:role.writers"
-                + "&authorization_details=details&expires_in=100&proxy_principal_spiffe_uris=");
-        assertNotNull(body);
-        assertEquals(body.getGrantType(), "client_credentials");
-        assertEquals(body.getScope(), "coretech:role.writers");
-        assertEquals(body.getAuthzDetails(), "details");
-        assertEquals(body.getExpiryTime(), 100);
-        assertNull(body.getProxyPrincipalsSpiffeUris());
-    }
-
-    @Test
-    public void testAccessTokenBodyInvalidGrant() {
-
-        try {
-            new AccessTokenBody("grant_type=unknown&scope=coretech:role.writers"
-                    + "&authorization_details=details");
-            fail();
-        } catch (IllegalArgumentException ex) {
-            assertEquals(ex.getMessage(), "Invalid grant request: unknown");
-        }
-    }
-
-    @Test
-    public void testAccessTokenBodyEmptyScope() {
-
-        try {
-            new AccessTokenBody("grant_type=client_credentials&scope=&expiry_time=100");
-            fail();
-        } catch (IllegalArgumentException ex) {
-            assertEquals(ex.getMessage(), "Invalid request: no scope provided");
-        }
-    }
-
-    @Test
-    public void testAccessTokenBodyValidSpiffeUri() {
-
-        // first valid uri test
-
-        AccessTokenBody body = new AccessTokenBody("grant_type=client_credentials&scope=test"
-                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service");
-        assertNotNull(body);
-        assertEquals(body.getGrantType(), "client_credentials");
-        assertEquals(body.getScope(), "test");
-        assertEquals(body.getProxyPrincipalsSpiffeUris().size(), 1);
-        assertEquals(body.getProxyPrincipalsSpiffeUris().get(0), "spiffe://data/sa/service");
-
-        // uri with leading space
-
-        body = new AccessTokenBody("grant_type=client_credentials&scope=test"
-                + "&proxy_principal_spiffe_uris= spiffe://data/sa/service");
-        assertNotNull(body);
-        assertEquals(body.getProxyPrincipalsSpiffeUris().get(0), "spiffe://data/sa/service");
-
-        // uri with multiple values
-
-        body = new AccessTokenBody("grant_type=client_credentials&scope=test"
-                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api");
-        assertNotNull(body);
-        assertEquals(body.getProxyPrincipalsSpiffeUris().size(), 2);
-        assertTrue(body.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service"));
-        assertTrue(body.getProxyPrincipalsSpiffeUris().contains("spiffe://sports/sa/api"));
-
-        // uri with spaces around the separator
-
-        body = new AccessTokenBody("grant_type=client_credentials&scope=test"
-                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service , spiffe://sports/sa/api");
-        assertNotNull(body);
-        assertEquals(body.getProxyPrincipalsSpiffeUris().size(), 2);
-        assertTrue(body.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service"));
-        assertTrue(body.getProxyPrincipalsSpiffeUris().contains("spiffe://sports/sa/api"));
-    }
-
-    @Test
-    public void testAccessTokenBodyInvalidSpiffeUri() {
-        try {
-            new AccessTokenBody("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=https://athenz.io");
-            fail();
-        } catch (IllegalArgumentException ex) {
-            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io");
-        }
-
-        try {
-            new AccessTokenBody("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://athenz/sa/service,https://athenz.io");
-            fail();
-        } catch (IllegalArgumentException ex) {
-            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io");
-        }
-
-        try {
-            new AccessTokenBody("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://a .io");
-            fail();
-        } catch (IllegalArgumentException ex) {
-            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: spiffe://a .io");
-        }
-    }
-}
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java
index 824d81d3f..a08e6fa85 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenRequestTest.java
@@ -15,190 +15,222 @@
  */
 package com.yahoo.athenz.zts.token;
 
-import com.yahoo.athenz.zts.ResourceException;
-import org.testng.annotations.BeforeMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.yahoo.athenz.auth.KeyStore;
+import com.yahoo.athenz.auth.Principal;
+import com.yahoo.athenz.auth.util.Crypto;
+import org.mockito.Mockito;
 import org.testng.annotations.Test;
 
+import java.io.File;
+import java.security.PrivateKey;
+import java.security.interfaces.ECPrivateKey;
+import java.time.Instant;
+import java.util.Date;
+
 import static org.testng.Assert.*;
 
 public class AccessTokenRequestTest {
 
-    @BeforeMethod
-    public void setup() {
-        AccessTokenRequest.setSupportOpenIdScope(true);
-    }
-
     @Test
     public void testAccessTokenRequest() {
 
-        AccessTokenRequest req1 = new AccessTokenRequest("sports:domain");
-        assertNotNull(req1);
-        assertEquals(req1.getDomainName(), "sports");
-        assertNull(req1.getRoleNames("sports"));
-        assertTrue(req1.sendScopeResponse());
-        assertFalse(req1.isOpenIdScope());
-
-        AccessTokenRequest req2 = new AccessTokenRequest("openid sports:service.api sports:domain");
-        assertNotNull(req2);
-        assertEquals(req2.getDomainName(), "sports");
-        assertNull(req2.getRoleNames("sports"));
-        assertTrue(req2.sendScopeResponse());
-        assertTrue(req2.isOpenIdScope());
-
-        // due to domain scope the role name one is ignored
-
-        AccessTokenRequest req3 = new AccessTokenRequest("openid sports:service.api sports:domain sports:role.role1");
-        assertNotNull(req3);
-        assertEquals(req3.getDomainName(), "sports");
-        assertNull(req3.getRoleNames("sports"));
-        assertTrue(req3.sendScopeResponse());
-        assertTrue(req3.isOpenIdScope());
-
-        AccessTokenRequest req4 = new AccessTokenRequest("sports:role.role1");
-        assertNotNull(req4);
-        assertEquals(req4.getDomainName(), "sports");
-        assertNotNull(req4.getRoleNames("sports"));
-        assertEquals(req4.getRoleNames("sports").length, 1);
-        assertEquals(req4.getRoleNames("sports")[0], "role1");
-        assertFalse(req4.sendScopeResponse());
-        assertFalse(req4.isOpenIdScope());
-
-        AccessTokenRequest req5 = new AccessTokenRequest("sports:role.role1 unknown-scope");
-        assertNotNull(req5);
-        assertEquals(req5.getDomainName(), "sports");
-        assertNotNull(req5.getRoleNames("sports"));
-        assertEquals(req5.getRoleNames("sports").length, 1);
-        assertEquals(req5.getRoleNames("sports")[0], "role1");
-        assertFalse(req5.sendScopeResponse());
-        assertFalse(req5.isOpenIdScope());
-
-        AccessTokenRequest req6 = new AccessTokenRequest("sports:role.role1 sports:role.role2");
-        assertNotNull(req6);
-        assertEquals(req6.getDomainName(), "sports");
-        assertNotNull(req6.getRoleNames("sports"));
-        assertEquals(req6.getRoleNames("sports").length, 2);
-        assertEquals(req6.getRoleNames("sports")[0], "role1");
-        assertEquals(req6.getRoleNames("sports")[1], "role2");
-        assertFalse(req6.sendScopeResponse());
-        assertFalse(req6.isOpenIdScope());
+        AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers"
+                + "&authorization_details=details&expires_in=100&proxy_principal_spiffe_uris=", null, null);
+        assertNotNull(request);
+        assertEquals(request.getGrantType(), "client_credentials");
+        assertEquals(request.getScope(), "coretech:role.writers");
+        assertEquals(request.getAuthzDetails(), "details");
+        assertEquals(request.getExpiryTime(), 100);
+        assertNull(request.getProxyPrincipalsSpiffeUris());
     }
 
     @Test
-    public void testAccessTokenRequestOpenidDisabled() {
-
-        AccessTokenRequest.setSupportOpenIdScope(false);
-
-        AccessTokenRequest req1 = new AccessTokenRequest("openid sports:service.api sports:domain");
-        assertNotNull(req1);
-        assertEquals(req1.getDomainName(), "sports");
-        assertNull(req1.getRoleNames("sports"));
-        assertTrue(req1.sendScopeResponse());
-        assertFalse(req1.isOpenIdScope());
-    }
-
-    @Test
-    public void testAccessTokenRequestInvalidDomains() {
+    public void testAccessTokenRequestInvalidGrant() {
 
         try {
-            new AccessTokenRequest("openid");
+            new AccessTokenRequest("grant_type=unknown&scope=coretech:role.writers"
+                    + "&authorization_details=details", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid grant request: unknown");
         }
+    }
 
-        try {
-            new AccessTokenRequest("unknown-scope");
-            fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
-        }
+    @Test
+    public void testAccessTokenRequestEmptyScope() {
 
         try {
-            new AccessTokenRequest(":role.role1");
+            new AccessTokenRequest("grant_type=client_credentials&scope=&expiry_time=100", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid request: no scope provided");
         }
+    }
+
+    @Test
+    public void testAccessTokenRequestValidSpiffeUri() {
+
+        // first valid uri test
+
+        AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=test"
+                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service", null, null);
+        assertNotNull(request);
+        assertEquals(request.getGrantType(), "client_credentials");
+        assertEquals(request.getScope(), "test");
+        assertEquals(request.getProxyPrincipalsSpiffeUris().size(), 1);
+        assertEquals(request.getProxyPrincipalsSpiffeUris().get(0), "spiffe://data/sa/service");
+
+        // uri with leading space
+
+        request = new AccessTokenRequest("grant_type=client_credentials&scope=test"
+                + "&proxy_principal_spiffe_uris= spiffe://data/sa/service", null, null);
+        assertNotNull(request);
+        assertEquals(request.getProxyPrincipalsSpiffeUris().get(0), "spiffe://data/sa/service");
+
+        // uri with multiple values
+
+        request = new AccessTokenRequest("grant_type=client_credentials&scope=test"
+                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service,spiffe://sports/sa/api", null, null);
+        assertNotNull(request);
+        assertEquals(request.getProxyPrincipalsSpiffeUris().size(), 2);
+        assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service"));
+        assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://sports/sa/api"));
+
+        // uri with spaces around the separator
+
+        request = new AccessTokenRequest("grant_type=client_credentials&scope=test"
+                + "&proxy_principal_spiffe_uris=spiffe://data/sa/service , spiffe://sports/sa/api", null, null);
+        assertNotNull(request);
+        assertEquals(request.getProxyPrincipalsSpiffeUris().size(), 2);
+        assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://data/sa/service"));
+        assertTrue(request.getProxyPrincipalsSpiffeUris().contains("spiffe://sports/sa/api"));
+    }
 
+    @Test
+    public void testAccessTokenRequestInvalidSpiffeUri() {
         try {
-            new AccessTokenRequest("sports:role.role1 :role.role2");
+            new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=https://athenz.io", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io");
         }
 
         try {
-            new AccessTokenRequest("sports:role.role1 openid weather:service.api");
+            new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://athenz/sa/service,https://athenz.io", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: https://athenz.io");
         }
 
         try {
-            new AccessTokenRequest("sports:role.role1 openid sports:service.api sports:service.backend");
+            new AccessTokenRequest("grant_type=client_credentials&scope=test&proxy_principal_spiffe_uris=spiffe://a .io", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid spiffe uri specified: spiffe://a .io");
         }
     }
 
     @Test
-    public void testAccessTokenRequestNoOpenidService() {
+    public void testAccessTokenRequestWithClientAssertionFailures() {
 
-        try {
-            new AccessTokenRequest("sports:domain openid");
-            fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
-        }
+        // missing client assertion type
 
         try {
-            new AccessTokenRequest("openid :domain");
+            new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers"
+                    + "&authorization_details=details&expires_in=100&client_assertion=jwt", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid request: no client assertion type provided");
         }
 
+        // unknown client assertion type
+
         try {
-            new AccessTokenRequest("sports:domain openid sports:service.");
+            new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers"
+                    + "&authorization_details=details&expires_in=100&client_assertion=jwt"
+                    + "&client_assertion_type=unknown", null, null);
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertEquals(ex.getMessage(), "Invalid client assertion type: unknown");
         }
 
+        // invalid token
+
+        KeyStore publicKeyProvider = Mockito.mock(KeyStore.class);
         try {
-            new AccessTokenRequest("sports:domain openid :service.api");
+            new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers"
+                    + "&authorization_details=details&expires_in=100&client_assertion=invalid-token"
+                    + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+                    publicKeyProvider, "https://athenz.io");
             fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
+        } catch (IllegalArgumentException ex) {
+            assertTrue(ex.getMessage().startsWith("Invalid client assertion: Unable to parse token: "));
         }
     }
 
     @Test
-    public void testAccessTokenRequestMultipleDomains() {
+    public void testAccessTokenRequestWithClientAssertion() throws JOSEException {
+
+        final File ecPrivateKey = new File("./src/test/resources/unit_test_zts_private_ec.pem");
+        final File ecPublicKey = new File("./src/test/resources/zts_public_ec.pem");
+
+        long now = System.currentTimeMillis() / 1000;
+        PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey);
+
+        JWSSigner signer = new ECDSASigner((ECPrivateKey) privateKey);
+        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("athenz.api")
+                .issueTime(Date.from(Instant.ofEpochSecond(now)))
+                .expirationTime(Date.from(Instant.ofEpochSecond(now + 3600)))
+                .issuer("athenz.api")
+                .audience("https://athenz.io/zts/v1")
+                .build();
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256).keyID("eckey1").build(), claimsSet);
+        signedJWT.sign(signer);
+        final String token = signedJWT.serialize();
+
+        KeyStore publicKeyProvider = Mockito.mock(KeyStore.class);
+        Mockito.when(publicKeyProvider.getServicePublicKey("athenz", "api", "eckey1"))
+                .thenReturn(Crypto.loadPublicKey(ecPublicKey));
+
+        AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials&scope=coretech:role.writers"
+                        + "&authorization_details=details&expires_in=100&client_assertion=" + token
+                        + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+                publicKeyProvider, "https://athenz.io/zts/v1");
+        assertNotNull(request);
+        assertEquals(request.getGrantType(), "client_credentials");
+        assertEquals(request.getScope(), "coretech:role.writers");
+        assertEquals(request.getAuthzDetails(), "details");
+        assertEquals(request.getExpiryTime(), 100);
+        assertNull(request.getProxyPrincipalsSpiffeUris());
+
+        Principal principal = request.getPrincipal();
+        assertNotNull(principal);
+        assertEquals(principal.getDomain(), "athenz");
+        assertEquals(principal.getName(), "api");
+    }
 
-        AccessTokenRequest req1 = new AccessTokenRequest("sports:domain sports:domain");
-        assertNotNull(req1);
+    @Test
+    public void testAccessTokenRequestQueryData() {
 
-        try {
-            new AccessTokenRequest("sports:domain weather:domain");
-            fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
-        }
+        AccessTokenRequest request = new AccessTokenRequest("grant_type=client_credentials"
+                + "&scope=data\ntest\ragain", null, null);
+        assertNotNull(request);
+        assertEquals(request.getQueryLogData(), "scope=data%0Atest%0Dagain");
 
-        try {
-            new AccessTokenRequest("sports:domain weather:role.role1");
-            fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
-        }
+        // generate a string with 1024 length
 
-        try {
-            new AccessTokenRequest("weather:role.role2 sports:domain weather:role.role1");
-            fail();
-        } catch (ResourceException ex) {
-            assertEquals(ex.getCode(), 400);
-        }
+        final String scope = "012345678901234".repeat(67);
+        request = new AccessTokenRequest("grant_type=client_credentials"
+                + "&scope=" + scope + "&expires_in=1024", null, null);
+        assertEquals(request.getQueryLogData(), "scope=" + scope + "&expires_in=1");
     }
 }
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenScopeTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenScopeTest.java
new file mode 100644
index 000000000..2d0f518e6
--- /dev/null
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/AccessTokenScopeTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yahoo.athenz.zts.token;
+
+import com.yahoo.athenz.zts.ResourceException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+public class AccessTokenScopeTest {
+
+    @BeforeMethod
+    public void setup() {
+        AccessTokenScope.setSupportOpenIdScope(true);
+    }
+
+    @Test
+    public void testAccessTokenScope() {
+
+        AccessTokenScope req1 = new AccessTokenScope("sports:domain");
+        assertNotNull(req1);
+        assertEquals(req1.getDomainName(), "sports");
+        assertNull(req1.getRoleNames("sports"));
+        assertTrue(req1.sendScopeResponse());
+        assertFalse(req1.isOpenIdScope());
+
+        AccessTokenScope req2 = new AccessTokenScope("openid sports:service.api sports:domain");
+        assertNotNull(req2);
+        assertEquals(req2.getDomainName(), "sports");
+        assertNull(req2.getRoleNames("sports"));
+        assertTrue(req2.sendScopeResponse());
+        assertTrue(req2.isOpenIdScope());
+
+        // due to domain scope the role name one is ignored
+
+        AccessTokenScope req3 = new AccessTokenScope("openid sports:service.api sports:domain sports:role.role1");
+        assertNotNull(req3);
+        assertEquals(req3.getDomainName(), "sports");
+        assertNull(req3.getRoleNames("sports"));
+        assertTrue(req3.sendScopeResponse());
+        assertTrue(req3.isOpenIdScope());
+
+        AccessTokenScope req4 = new AccessTokenScope("sports:role.role1");
+        assertNotNull(req4);
+        assertEquals(req4.getDomainName(), "sports");
+        assertNotNull(req4.getRoleNames("sports"));
+        assertEquals(req4.getRoleNames("sports").length, 1);
+        assertEquals(req4.getRoleNames("sports")[0], "role1");
+        assertFalse(req4.sendScopeResponse());
+        assertFalse(req4.isOpenIdScope());
+
+        AccessTokenScope req5 = new AccessTokenScope("sports:role.role1 unknown-scope");
+        assertNotNull(req5);
+        assertEquals(req5.getDomainName(), "sports");
+        assertNotNull(req5.getRoleNames("sports"));
+        assertEquals(req5.getRoleNames("sports").length, 1);
+        assertEquals(req5.getRoleNames("sports")[0], "role1");
+        assertFalse(req5.sendScopeResponse());
+        assertFalse(req5.isOpenIdScope());
+
+        AccessTokenScope req6 = new AccessTokenScope("sports:role.role1 sports:role.role2");
+        assertNotNull(req6);
+        assertEquals(req6.getDomainName(), "sports");
+        assertNotNull(req6.getRoleNames("sports"));
+        assertEquals(req6.getRoleNames("sports").length, 2);
+        assertEquals(req6.getRoleNames("sports")[0], "role1");
+        assertEquals(req6.getRoleNames("sports")[1], "role2");
+        assertFalse(req6.sendScopeResponse());
+        assertFalse(req6.isOpenIdScope());
+    }
+
+    @Test
+    public void testAccessTokenScopeOpenidDisabled() {
+
+        AccessTokenScope.setSupportOpenIdScope(false);
+
+        AccessTokenScope req1 = new AccessTokenScope("openid sports:service.api sports:domain");
+        assertNotNull(req1);
+        assertEquals(req1.getDomainName(), "sports");
+        assertNull(req1.getRoleNames("sports"));
+        assertTrue(req1.sendScopeResponse());
+        assertFalse(req1.isOpenIdScope());
+    }
+
+    @Test
+    public void testAccessTokenScopeInvalidDomains() {
+
+        try {
+            new AccessTokenScope("openid");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("unknown-scope");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope(":role.role1");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:role.role1 :role.role2");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:role.role1 openid weather:service.api");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:role.role1 openid sports:service.api sports:service.backend");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+    }
+
+    @Test
+    public void testAccessTokenScopeNoOpenidService() {
+
+        try {
+            new AccessTokenScope("sports:domain openid");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("openid :domain");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:domain openid sports:service.");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:domain openid :service.api");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+    }
+
+    @Test
+    public void testAccessTokenScopeMultipleDomains() {
+
+        AccessTokenScope req1 = new AccessTokenScope("sports:domain sports:domain");
+        assertNotNull(req1);
+
+        try {
+            new AccessTokenScope("sports:domain weather:domain");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("sports:domain weather:role.role1");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+
+        try {
+            new AccessTokenScope("weather:role.role2 sports:domain weather:role.role1");
+            fail();
+        } catch (ResourceException ex) {
+            assertEquals(ex.getCode(), 400);
+        }
+    }
+}
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenRequestTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenScopeTest.java
similarity index 74%
rename from servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenRequestTest.java
rename to servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenScopeTest.java
index 1626bd99a..4923d41bf 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenRequestTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/IdTokenScopeTest.java
@@ -20,19 +20,19 @@ import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
-public class IdTokenRequestTest {
+public class IdTokenScopeTest {
 
     @Test
-    public void testIdTokenRequest() {
+    public void testIdTokenScope() {
 
         //   openid
         //   openid [groups | roles]
         //   openid <domainName>:role.<roleName>
         //   openid <domainName>:group.<groupName>
 
-        IdTokenRequest.setMaxDomains(1);
+        IdTokenScope.setMaxDomains(1);
 
-        IdTokenRequest req1 = new IdTokenRequest("openid");
+        IdTokenScope req1 = new IdTokenScope("openid");
         assertNotNull(req1);
         assertNull(req1.getDomainName());
         assertNull(req1.getRoleNames("sports"));
@@ -41,7 +41,7 @@ public class IdTokenRequestTest {
         assertFalse(req1.isGroupsScope());
         assertFalse(req1.isRolesScope());
 
-        IdTokenRequest req2 = new IdTokenRequest("openid groups");
+        IdTokenScope req2 = new IdTokenScope("openid groups");
         assertNotNull(req2);
         assertNull(req2.getDomainName());
         assertNull(req2.getRoleNames("sports"));
@@ -50,7 +50,7 @@ public class IdTokenRequestTest {
         assertTrue(req2.isGroupsScope());
         assertFalse(req2.isRolesScope());
 
-        IdTokenRequest req3 = new IdTokenRequest("openid roles");
+        IdTokenScope req3 = new IdTokenScope("openid roles");
         assertNotNull(req3);
         assertNull(req3.getDomainName());
         assertNull(req3.getRoleNames("sports"));
@@ -59,7 +59,7 @@ public class IdTokenRequestTest {
         assertFalse(req3.isGroupsScope());
         assertTrue(req3.isRolesScope());
 
-        IdTokenRequest req4 = new IdTokenRequest("openid sports:group.dev-team");
+        IdTokenScope req4 = new IdTokenScope("openid sports:group.dev-team");
         assertNotNull(req4);
         assertEquals(req4.getDomainName(), "sports");
         assertNull(req4.getRoleNames("sports"));
@@ -69,7 +69,7 @@ public class IdTokenRequestTest {
         assertTrue(req4.isGroupsScope());
         assertFalse(req4.isRolesScope());
 
-        IdTokenRequest req5 = new IdTokenRequest("openid sports:role.dev-role");
+        IdTokenScope req5 = new IdTokenScope("openid sports:role.dev-role");
         assertNotNull(req5);
         assertEquals(req5.getDomainName(), "sports");
         assertEquals(req5.getRoleNames("sports").length, 1);
@@ -79,7 +79,7 @@ public class IdTokenRequestTest {
         assertFalse(req5.isGroupsScope());
         assertTrue(req5.isRolesScope());
 
-        IdTokenRequest req6 = new IdTokenRequest("openid sports:service.api sports:domain sports:group.dev-team");
+        IdTokenScope req6 = new IdTokenScope("openid sports:service.api sports:domain sports:group.dev-team");
         assertNotNull(req6);
         assertEquals(req6.getDomainName(), "sports");
         assertNull(req6.getRoleNames("sports"));
@@ -89,7 +89,7 @@ public class IdTokenRequestTest {
         assertTrue(req6.isGroupsScope());
         assertFalse(req6.isRolesScope());
 
-        IdTokenRequest req7 = new IdTokenRequest("openid sports:service.api sports:role.reader sports:group.dev-team");
+        IdTokenScope req7 = new IdTokenScope("openid sports:service.api sports:role.reader sports:group.dev-team");
         assertNotNull(req7);
         assertEquals(req7.getDomainName(), "sports");
         assertEquals(req7.getRoleNames("sports").length, 1);
@@ -101,7 +101,7 @@ public class IdTokenRequestTest {
         assertTrue(req7.isGroupsScope());
         assertTrue(req7.isRolesScope());
 
-        IdTokenRequest req8 = new IdTokenRequest("openid groups unknown-scope");
+        IdTokenScope req8 = new IdTokenScope("openid groups unknown-scope");
         assertNotNull(req8);
         assertNull(req8.getDomainName());
         assertNull(req8.getRoleNames("sports"));
@@ -113,68 +113,68 @@ public class IdTokenRequestTest {
     }
 
     @Test
-    public void testIdTokenRequestNoOpenid() {
+    public void testIdTokenScopeNoOpenid() {
 
-        IdTokenRequest.setMaxDomains(1);
+        IdTokenScope.setMaxDomains(1);
 
         try {
-            new IdTokenRequest("groups");
+            new IdTokenScope("groups");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("unknown-scope");
+            new IdTokenScope("unknown-scope");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest(":role.role1");
+            new IdTokenScope(":role.role1");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("sports:role.role1 :role.role2");
+            new IdTokenScope("sports:role.role1 :role.role2");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("openid sports:group.dev-team :group.prod-team");
+            new IdTokenScope("openid sports:group.dev-team :group.prod-team");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("openid :group.prod-team");
+            new IdTokenScope("openid :group.prod-team");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("sports:role.role1 openid weather:service.api");
+            new IdTokenScope("sports:role.role1 openid weather:service.api");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("sports:group.dev-team openid weather:group.dev-team");
+            new IdTokenScope("sports:group.dev-team openid weather:group.dev-team");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("sports:role.role1 openid sports:service.api sports:service.backend");
+            new IdTokenScope("sports:role.role1 openid sports:service.api sports:service.backend");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
@@ -182,29 +182,29 @@ public class IdTokenRequestTest {
     }
 
     @Test
-    public void testIdTokenRequestMultipleDomains() {
+    public void testIdTokenScopeMultipleDomains() {
 
-        IdTokenRequest.setMaxDomains(1);
+        IdTokenScope.setMaxDomains(1);
 
-        IdTokenRequest req1 = new IdTokenRequest("openid sports:domain sports:domain");
+        IdTokenScope req1 = new IdTokenScope("openid sports:domain sports:domain");
         assertNotNull(req1);
 
         try {
-            new IdTokenRequest("openid sports:domain weather:domain");
+            new IdTokenScope("openid sports:domain weather:domain");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("openid sports:domain weather:role.role1");
+            new IdTokenScope("openid sports:domain weather:role.role1");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
         }
 
         try {
-            new IdTokenRequest("openid weather:role.role2 sports:domain weather:role.role1");
+            new IdTokenScope("openid weather:role.role2 sports:domain weather:role.role1");
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), 400);
diff --git a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenRequestTest.java b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenScopeTest.java
similarity index 71%
rename from servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenRequestTest.java
rename to servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenScopeTest.java
index a0cce5077..1791fdf7e 100644
--- a/servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenRequestTest.java
+++ b/servers/zts/src/test/java/com/yahoo/athenz/zts/token/OAuthTokenScopeTest.java
@@ -23,13 +23,13 @@ import java.util.Set;
 
 import static org.testng.Assert.*;
 
-public class OAuthTokenRequestTest {
+public class OAuthTokenScopeTest {
 
     @Test
-    public void testOauthTokenInvalidRequest() {
+    public void testOauthTokenInvalidScope() {
 
         try {
-            new OAuthTokenRequest("scope", 0, null);
+            new OAuthTokenScope("scope", 0, null);
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), ResourceException.BAD_REQUEST);
@@ -38,17 +38,17 @@ public class OAuthTokenRequestTest {
     }
 
     @Test
-    public void testOauthTokenRequestMaxDomains() {
+    public void testOauthTokenScopeMaxDomains() {
 
         try {
-            new OAuthTokenRequest("openid sports:domain weather:domain finance:domain", 2, null);
+            new OAuthTokenScope("openid sports:domain weather:domain finance:domain", 2, null);
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), ResourceException.BAD_REQUEST);
             assertTrue(ex.getMessage().contains("Domain limit: 2 has been reached"));
         }
 
-        OAuthTokenRequest request = new OAuthTokenRequest("openid sports:domain weather:domain finance:domain", 3, null);
+        OAuthTokenScope request = new OAuthTokenScope("openid sports:domain weather:domain finance:domain", 3, null);
         assertNotNull(request);
         Set<String> domains = request.getDomainNames();
         assertEquals(domains.size(), 3);
@@ -59,30 +59,30 @@ public class OAuthTokenRequestTest {
 
     @Test
     public void testOauthTokenGetDomainName() {
-        OAuthTokenRequest request = new OAuthTokenRequest("openid", 1, null);
+        OAuthTokenScope request = new OAuthTokenScope("openid", 1, null);
         assertNull(request.getDomainName());
 
-        request = new OAuthTokenRequest("openid sports:domain weather:domain", 2, null);
+        request = new OAuthTokenScope("openid sports:domain weather:domain", 2, null);
         assertNull(request.getDomainName());
 
-        request = new OAuthTokenRequest("openid sports:domain", 2, null);
+        request = new OAuthTokenScope("openid sports:domain", 2, null);
         assertNull(request.getDomainName());
 
-        request = new OAuthTokenRequest("openid sports:domain", 1, null);
+        request = new OAuthTokenScope("openid sports:domain", 1, null);
         assertEquals(request.getDomainName(), "sports");
     }
 
     @Test
     public void testOauthTokenGetRoleNames() {
-        OAuthTokenRequest request = new OAuthTokenRequest("openid", 1, null);
+        OAuthTokenScope request = new OAuthTokenScope("openid", 1, null);
         assertNull(request.getRoleNames("sports"));
 
-        request = new OAuthTokenRequest("openid weather:role.test", 1, null);
+        request = new OAuthTokenScope("openid weather:role.test", 1, null);
         assertNull(request.getRoleNames("sports"));
         assertEquals(request.getRoleNames("weather").length, 1);
         assertEquals(request.getRoleNames("weather")[0], "test");
 
-        request = new OAuthTokenRequest("openid weather:role.test weather:role.admin", 2, null);
+        request = new OAuthTokenScope("openid weather:role.test weather:role.admin", 2, null);
         assertNull(request.getRoleNames("sports"));
         assertEquals(request.getRoleNames("weather").length, 2);
         assertEquals(request.getRoleNames("weather")[0], "test");
@@ -91,21 +91,21 @@ public class OAuthTokenRequestTest {
 
     @Test
     public void testOauthTokenMultipleIdenticalServiceNames() {
-        OAuthTokenRequest request = new OAuthTokenRequest("openid sports:service.api sports:service.api", 1, null);
+        OAuthTokenScope request = new OAuthTokenScope("openid sports:service.api sports:service.api", 1, null);
         assertNull(request.getRoleNames("sports"));
         assertEquals(request.getServiceName(), "api");
         assertEquals(request.getDomainName(), "sports");
     }
 
     @Test
-    public void testOauthTokenRequestMaxDomainsWithAuthorizedRoles() {
+    public void testOauthTokenScopeMaxDomainsWithAuthorizedRoles() {
 
         DynamicConfigCsv systemAllowedRoles = new DynamicConfigCsv("system:role.reader,system:role.writer");
 
         // without authorized roles we'll have failure
 
         try {
-            new OAuthTokenRequest("openid system:role.reader sports:service.api", 1, null);
+            new OAuthTokenScope("openid system:role.reader sports:service.api", 1, null);
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), ResourceException.BAD_REQUEST);
@@ -114,7 +114,7 @@ public class OAuthTokenRequestTest {
 
         // with authorized list, we're good
 
-        OAuthTokenRequest request = new OAuthTokenRequest("openid system:role.reader sports:service.api",
+        OAuthTokenScope request = new OAuthTokenScope("openid system:role.reader sports:service.api",
                 1, systemAllowedRoles);
         Set<String> domains = request.getDomainNames();
         assertEquals(domains.size(), 2);
@@ -125,7 +125,7 @@ public class OAuthTokenRequestTest {
         // with 2 authorized roles - not allowed
 
         try {
-            new OAuthTokenRequest("openid system:role.reader sports:service.api system:role.writer", 1, null);
+            new OAuthTokenScope("openid system:role.reader sports:service.api system:role.writer", 1, null);
             fail();
         } catch (ResourceException ex) {
             assertEquals(ex.getCode(), ResourceException.BAD_REQUEST);
@@ -134,7 +134,7 @@ public class OAuthTokenRequestTest {
 
         // standard single role without any system should work fine
 
-        request = new OAuthTokenRequest("openid sports:service.api", 1, systemAllowedRoles);
+        request = new OAuthTokenScope("openid sports:service.api", 1, systemAllowedRoles);
         domains = request.getDomainNames();
         assertEquals(domains.size(), 1);
         assertTrue(domains.contains("sports"));
diff --git a/ui/src/__tests__/spec/tests/domain.spec.js b/ui/src/__tests__/spec/tests/domain.spec.js
index 198f4e42c..d71527f9c 100644
--- a/ui/src/__tests__/spec/tests/domain.spec.js
+++ b/ui/src/__tests__/spec/tests/domain.spec.js
@@ -77,7 +77,7 @@ describe('Domain', () => {
         );
         await expand.click();
 
-        // click add business service
+        // click add slack channel
         let addSlackChannel = await $('a[data-testid="add-slack-channel"]');
         await browser.waitUntil(
             async () => await addSlackChannel.isClickable()
@@ -87,6 +87,7 @@ describe('Domain', () => {
         let randomInt = Math.floor(Math.random() * 100); // random number to append to slack channel name
         let slackChannelName = 'slack-channel-' + randomInt;
         let slackChannelInput = await $('input[name="slack-channel-input"]');
+        await slackChannelInput.clearValue();
         await slackChannelInput.addValue(slackChannelName);
         let submitButton = await $('button*=Submit');
         await submitButton.click();