vibe-proxy/backend/internal/api/handlers/management/auth_files_fields.go
2026-08-24 00:10:41 +02:00

866 lines
23 KiB
Go

package management
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/credentialweight"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// PatchAuthFileStatus toggles the disabled state of an auth file
func (h *Handler) PatchAuthFileStatus(c *gin.Context) {
if h.authManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"})
return
}
var req struct {
Name string `json:"name"`
AuthIndex string `json:"auth_index"`
Disabled *bool `json:"disabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
name := strings.TrimSpace(req.Name)
authIndex := strings.TrimSpace(req.AuthIndex)
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
if req.Disabled == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "disabled is required"})
return
}
ctx := c.Request.Context()
targetAuth, _ := h.lookupAuthFile(name, authIndex)
if targetAuth == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "auth file not found"})
return
}
if coreauth.IsPluginVirtualAuth(targetAuth) {
// Allow status changes only when targeting the source auth file name, matching delete semantics.
// Expanded virtual project auths still cannot be modified independently.
if !isPluginVirtualSourceDelete(name, targetAuth) {
c.JSON(http.StatusConflict, gin.H{"error": errPluginVirtualAuth.Error()})
return
}
if errPatch := h.patchPluginVirtualSourceStatus(ctx, targetAuth, *req.Disabled); errPatch != nil {
status := http.StatusInternalServerError
if errors.Is(errPatch, errAuthFileNotFound) || os.IsNotExist(errPatch) {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": errPatch.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "disabled": *req.Disabled})
return
}
if coreauth.IsConfigAPIKeyAuth(targetAuth) {
h.mu.Lock()
handled, errToggle := toggleConfigAPIKeyExcludedAll(h.cfg, targetAuth, *req.Disabled)
if errToggle != nil {
h.mu.Unlock()
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to update config api key: %v", errToggle)})
return
}
if !handled {
h.mu.Unlock()
c.JSON(http.StatusNotFound, gin.H{"error": "config api key entry not found"})
return
}
cfgSnapshot, okSnapshot := h.saveConfigAndSnapshotLocked(c)
h.mu.Unlock()
if !okSnapshot {
return
}
h.reloadConfigAfterManagementSave(ctx, cfgSnapshot)
if h.tokenStore != nil {
_ = h.tokenStore.Delete(ctx, targetAuth.ID)
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"disabled": *req.Disabled,
"via": "config:excluded-models",
"excluded_pattern": configAPIKeyDisablePattern,
})
return
}
applyAuthDisabledState(targetAuth, *req.Disabled)
if _, err := h.authManager.Update(ctx, targetAuth); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to update auth: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "disabled": *req.Disabled})
}
// patchPluginVirtualSourceStatus toggles disabled on a plugin multi-auth source file and all
// runtime auths expanded from it. Virtual project children cannot be toggled independently.
func (h *Handler) patchPluginVirtualSourceStatus(ctx context.Context, targetAuth *coreauth.Auth, disabled bool) error {
if h == nil || h.authManager == nil || targetAuth == nil {
return fmt.Errorf("core auth manager unavailable")
}
sourcePath := strings.TrimSpace(authAttribute(targetAuth, coreauth.AttributeVirtualSource))
if sourcePath == "" {
sourcePath = strings.TrimSpace(authAttribute(targetAuth, "path"))
}
if sourcePath == "" {
return errPluginVirtualAuth
}
if errWrite := setSourceAuthFileDisabled(sourcePath, disabled); errWrite != nil {
if os.IsNotExist(errWrite) {
return errAuthFileNotFound
}
return fmt.Errorf("failed to update source auth file: %w", errWrite)
}
now := time.Now()
for _, auth := range h.authManager.List() {
if auth == nil {
continue
}
if !sameAuthFilePath(authAttribute(auth, "path"), sourcePath) &&
!sameAuthFilePath(authAttribute(auth, coreauth.AttributeVirtualSource), sourcePath) {
continue
}
applyAuthDisabledState(auth, disabled)
auth.UpdatedAt = now
if _, errUpdate := h.authManager.Update(ctx, auth); errUpdate != nil {
return fmt.Errorf("failed to update auth %s: %w", auth.ID, errUpdate)
}
}
return nil
}
func setSourceAuthFileDisabled(path string, disabled bool) error {
path = strings.TrimSpace(path)
if path == "" {
return fmt.Errorf("source auth path is empty")
}
data, errRead := os.ReadFile(path)
if errRead != nil {
return errRead
}
metadata := make(map[string]any)
if len(bytes.TrimSpace(data)) > 0 {
if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil {
return fmt.Errorf("invalid auth file: %w", errUnmarshal)
}
}
if metadata == nil {
metadata = make(map[string]any)
}
coreauth.NormalizeCredentialMetadata(metadata)
metadata["disabled"] = disabled
raw, errMarshal := json.Marshal(metadata)
if errMarshal != nil {
return fmt.Errorf("marshal auth file: %w", errMarshal)
}
if errWrite := os.WriteFile(path, raw, 0o600); errWrite != nil {
return errWrite
}
return nil
}
func applyAuthDisabledState(auth *coreauth.Auth, disabled bool) {
if auth == nil {
return
}
auth.Disabled = disabled
if disabled {
auth.Status = coreauth.StatusDisabled
auth.StatusMessage = "disabled via management API"
} else {
auth.Status = coreauth.StatusActive
auth.StatusMessage = ""
}
auth.UpdatedAt = time.Now()
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
auth.Metadata["disabled"] = disabled
}
// PatchAuthFileFields updates arbitrary metadata fields of an auth file.
func (h *Handler) PatchAuthFileFields(c *gin.Context) {
if h.authManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"})
return
}
var req map[string]json.RawMessage
decoder := json.NewDecoder(c.Request.Body)
decoder.UseNumber()
if err := decoder.Decode(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
nameRaw, ok := req["name"]
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
var nameValue string
if err := json.Unmarshal(nameRaw, &nameValue); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
name := strings.TrimSpace(nameValue)
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
delete(req, "name")
var errNormalize error
req, errNormalize = normalizeAuthFilePatchFields(req)
if errNormalize != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": errNormalize.Error()})
return
}
requestRetryPatch, errRequestRetry := decodeAuthFileRequestRetryPatch(req)
if errRequestRetry != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": errRequestRetry.Error()})
return
}
for key := range req {
if strings.TrimSpace(key) == "request_retry" {
delete(req, key)
}
}
ctx := c.Request.Context()
// Find auth by name or ID
var targetAuth *coreauth.Auth
if auth, ok := h.authManager.GetByID(name); ok {
targetAuth = auth
} else {
auths := h.authManager.List()
for _, auth := range auths {
if auth.FileName == name {
targetAuth = auth
break
}
}
}
if targetAuth == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "auth file not found"})
return
}
if coreauth.IsPluginVirtualAuth(targetAuth) {
c.JSON(http.StatusConflict, gin.H{"error": errPluginVirtualAuth.Error()})
return
}
coreauth.NormalizeCredentialMetadata(targetAuth.Metadata)
changed := false
touchedRoots := make(map[string]struct{}, len(req))
for key, rawValue := range req {
fieldPath := strings.TrimSpace(key)
if fieldPath == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "field name is required"})
return
}
value, errDecode := decodeAuthFileFieldValue(rawValue)
if errDecode != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid field %s", fieldPath)})
return
}
if targetAuth.Metadata == nil {
targetAuth.Metadata = make(map[string]any)
}
if fieldPath == coreauth.AttributeWeight {
if value == nil {
delete(targetAuth.Metadata, coreauth.AttributeWeight)
} else {
if _, okNumber := value.(json.Number); !okNumber {
c.JSON(http.StatusBadRequest, gin.H{"error": "weight must be an integer"})
return
}
weight, errWeight := credentialweight.ParseValue(value)
if errWeight != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": errWeight.Error()})
return
}
targetAuth.Metadata[coreauth.AttributeWeight] = weight
}
} else if rootAuthFileField(fieldPath) == coreauth.AttributeWeight {
c.JSON(http.StatusBadRequest, gin.H{"error": "weight does not support nested fields"})
return
} else if fieldPath == "headers" {
applyAuthFileHeadersPatch(targetAuth, value)
} else if errSet := setAuthFileMetadataValue(targetAuth.Metadata, fieldPath, value); errSet != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": errSet.Error()})
return
}
if root := rootAuthFileField(fieldPath); root != "" {
touchedRoots[root] = struct{}{}
}
changed = true
}
if requestRetryPatch.Set {
if targetAuth.Metadata == nil {
targetAuth.Metadata = make(map[string]any)
}
if requestRetryPatch.Value == nil {
delete(targetAuth.Metadata, "request_retry")
} else {
targetAuth.Metadata["request_retry"] = *requestRetryPatch.Value
}
changed = true
}
if changed {
syncAuthFileMetadataFields(targetAuth, touchedRoots)
}
if !changed {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
targetAuth.UpdatedAt = time.Now()
if _, err := h.authManager.Update(ctx, targetAuth); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to update auth: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
func decodeAuthFileFieldValue(raw json.RawMessage) (any, error) {
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
var value any
if err := decoder.Decode(&value); err != nil {
return nil, err
}
return value, nil
}
type authFileRequestRetryPatch struct {
Set bool
Value *int
}
func normalizeAuthFilePatchFields(fields map[string]json.RawMessage) (map[string]json.RawMessage, error) {
normalized := make(map[string]json.RawMessage, len(fields))
originalNames := make(map[string]string, len(fields))
canonicalNames := make(map[string]bool, len(fields))
for key, value := range fields {
parts := strings.Split(strings.TrimSpace(key), ".")
for index := range parts {
parts[index] = strings.TrimSpace(parts[index])
}
originalRoot := parts[0]
parts[0] = coreauth.CanonicalCredentialMetadataKey(originalRoot)
canonicalPath := strings.Join(parts, ".")
if original, exists := originalNames[canonicalPath]; exists {
currentCanonical := originalRoot == parts[0]
if canonicalNames[canonicalPath] != currentCanonical {
if currentCanonical {
normalized[canonicalPath] = value
originalNames[canonicalPath] = key
canonicalNames[canonicalPath] = true
}
continue
}
return nil, fmt.Errorf("auth file fields %q and %q refer to the same field", original, key)
}
normalized[canonicalPath] = value
originalNames[canonicalPath] = key
canonicalNames[canonicalPath] = originalRoot == parts[0]
}
return normalized, nil
}
func decodeAuthFileRequestRetryPatch(fields map[string]json.RawMessage) (authFileRequestRetryPatch, error) {
var raw json.RawMessage
found := false
for key, value := range fields {
fieldPath := strings.TrimSpace(key)
fieldRoot := rootAuthFileField(fieldPath)
if fieldRoot == "request_retry" && fieldPath != fieldRoot {
return authFileRequestRetryPatch{}, fmt.Errorf("request_retry does not support nested fields")
}
if fieldPath == "request_retry" {
found = true
raw = value
}
}
if !found {
return authFileRequestRetryPatch{}, nil
}
value, errDecode := decodeAuthFileFieldValue(raw)
if errDecode != nil {
return authFileRequestRetryPatch{}, fmt.Errorf("request_retry must be an integer or null")
}
if value == nil {
return authFileRequestRetryPatch{Set: true}, nil
}
number, okNumber := value.(json.Number)
if !okNumber {
return authFileRequestRetryPatch{}, fmt.Errorf("request_retry must be an integer or null")
}
parsed, errInt := number.Int64()
if errInt != nil {
return authFileRequestRetryPatch{}, fmt.Errorf("request_retry must be an integer or null")
}
normalized := int(parsed)
if int64(normalized) != parsed {
return authFileRequestRetryPatch{}, fmt.Errorf("request_retry must be an integer or null")
}
if normalized < 0 {
return authFileRequestRetryPatch{Set: true}, nil
}
return authFileRequestRetryPatch{Set: true, Value: &normalized}, nil
}
func rootAuthFileField(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
if idx := strings.Index(path, "."); idx >= 0 {
return strings.TrimSpace(path[:idx])
}
return path
}
func setAuthFileMetadataValue(metadata map[string]any, path string, value any) error {
if metadata == nil {
return fmt.Errorf("metadata is nil")
}
parts := strings.Split(path, ".")
current := metadata
for i, rawPart := range parts {
part := strings.TrimSpace(rawPart)
if part == "" {
return fmt.Errorf("invalid field path: %s", path)
}
if i == len(parts)-1 {
current[part] = value
return nil
}
next, ok := current[part].(map[string]any)
if !ok {
next = make(map[string]any)
current[part] = next
}
current = next
}
return nil
}
func applyAuthFileHeadersPatch(auth *coreauth.Auth, value any) {
if auth == nil {
return
}
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
headersPatch, ok := authFileHeadersStringMap(value)
if !ok {
auth.Metadata["headers"] = value
return
}
existingHeaders := coreauth.ExtractCustomHeadersFromMetadata(auth.Metadata)
nextHeaders := make(map[string]string, len(existingHeaders))
for key, val := range existingHeaders {
nextHeaders[key] = val
}
for key, value := range headersPatch {
name := strings.TrimSpace(key)
if name == "" {
continue
}
val := strings.TrimSpace(value)
if val == "" {
delete(nextHeaders, name)
continue
}
nextHeaders[name] = val
}
if len(nextHeaders) == 0 {
delete(auth.Metadata, "headers")
return
}
metaHeaders := make(map[string]any, len(nextHeaders))
for key, value := range nextHeaders {
metaHeaders[key] = value
}
auth.Metadata["headers"] = metaHeaders
}
func authFileHeadersStringMap(value any) (map[string]string, bool) {
switch typed := value.(type) {
case map[string]string:
return typed, true
case map[string]any:
out := make(map[string]string, len(typed))
for key, rawValue := range typed {
value, ok := rawValue.(string)
if !ok {
return nil, false
}
out[key] = value
}
return out, true
default:
return nil, false
}
}
func syncAuthFileMetadataFields(auth *coreauth.Auth, touchedRoots map[string]struct{}) {
if auth == nil || len(touchedRoots) == 0 {
return
}
if _, ok := touchedRoots["prefix"]; ok {
if prefix, okString := auth.Metadata["prefix"].(string); okString {
auth.Prefix = strings.TrimSpace(prefix)
}
}
if _, ok := touchedRoots["proxy_url"]; ok {
if proxyURL, okString := auth.Metadata["proxy_url"].(string); okString {
auth.ProxyURL = strings.TrimSpace(proxyURL)
}
}
if _, ok := touchedRoots["headers"]; ok {
syncAuthFileHeaderAttributes(auth)
}
if _, ok := touchedRoots["priority"]; ok {
syncAuthFilePriorityAttribute(auth)
}
if _, ok := touchedRoots[coreauth.AttributeWeight]; ok {
syncAuthFileWeightAttribute(auth)
}
if _, ok := touchedRoots["note"]; ok {
syncAuthFileNoteAttribute(auth)
}
if _, ok := touchedRoots["websockets"]; ok {
syncAuthFileWebsocketsAttribute(auth)
}
if _, ok := touchedRoots["disabled"]; ok {
syncAuthFileDisabledState(auth)
}
}
func syncAuthFileHeaderAttributes(auth *coreauth.Auth) {
if auth == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
for key := range auth.Attributes {
if strings.HasPrefix(key, "header:") {
delete(auth.Attributes, key)
}
}
for name, value := range coreauth.ExtractCustomHeadersFromMetadata(auth.Metadata) {
auth.Attributes["header:"+name] = value
}
}
func syncAuthFilePriorityAttribute(auth *coreauth.Auth) {
if auth == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
priority, ok := authFileIntValue(auth.Metadata["priority"])
if !ok {
delete(auth.Attributes, "priority")
return
}
if priority == 0 {
delete(auth.Attributes, "priority")
return
}
auth.Attributes["priority"] = strconv.Itoa(priority)
}
func syncAuthFileWeightAttribute(auth *coreauth.Auth) {
if auth == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
weight, errWeight := credentialweight.ParseValue(auth.Metadata[coreauth.AttributeWeight])
if errWeight != nil {
delete(auth.Attributes, coreauth.AttributeWeight)
return
}
auth.Attributes[coreauth.AttributeWeight] = strconv.FormatInt(weight, 10)
}
func authFileIntValue(value any) (int, bool) {
switch typed := value.(type) {
case int:
return typed, true
case int64:
return int(typed), true
case float64:
return int(typed), true
case json.Number:
if i, err := typed.Int64(); err == nil {
return int(i), true
}
case string:
if i, err := strconv.Atoi(strings.TrimSpace(typed)); err == nil {
return i, true
}
}
return 0, false
}
func syncAuthFileNoteAttribute(auth *coreauth.Auth) {
if auth == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
note, ok := auth.Metadata["note"].(string)
if !ok {
delete(auth.Attributes, "note")
return
}
note = strings.TrimSpace(note)
if note == "" {
delete(auth.Attributes, "note")
return
}
auth.Attributes["note"] = note
}
func syncAuthFileWebsocketsAttribute(auth *coreauth.Auth) {
if auth == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
websockets, ok := authFileBoolValue(auth.Metadata["websockets"])
if !ok {
delete(auth.Attributes, "websockets")
return
}
auth.Attributes["websockets"] = strconv.FormatBool(websockets)
}
func authFileBoolValue(value any) (bool, bool) {
switch typed := value.(type) {
case bool:
return typed, true
case string:
parsed, errParse := strconv.ParseBool(strings.TrimSpace(typed))
if errParse == nil {
return parsed, true
}
}
return false, false
}
func syncAuthFileDisabledState(auth *coreauth.Auth) {
if auth == nil {
return
}
disabled, ok := authFileBoolValue(auth.Metadata["disabled"])
if !ok {
return
}
auth.Disabled = disabled
if disabled {
auth.Status = coreauth.StatusDisabled
if strings.TrimSpace(auth.StatusMessage) == "" {
auth.StatusMessage = "disabled via management API"
}
return
}
auth.Status = coreauth.StatusActive
auth.StatusMessage = ""
}
func (h *Handler) removeAuth(ctx context.Context, id string) {
if h == nil || h.authManager == nil {
return
}
id = strings.TrimSpace(id)
if id == "" {
return
}
if _, ok := h.authManager.GetByID(id); ok {
h.authManager.Remove(ctx, id)
return
}
authID := h.authIDForPath(id)
if authID == "" {
return
}
h.authManager.Remove(ctx, authID)
}
func (h *Handler) removeAuthsForPath(ctx context.Context, path string, fallbackID string) {
if h == nil || h.authManager == nil {
return
}
removed := false
for _, auth := range h.authManager.List() {
if auth == nil {
continue
}
if sameAuthFilePath(authAttribute(auth, "path"), path) || sameAuthFilePath(authAttribute(auth, coreauth.AttributeVirtualSource), path) {
h.removeAuth(ctx, auth.ID)
removed = true
}
}
if removed {
return
}
if strings.TrimSpace(fallbackID) != "" {
h.removeAuth(ctx, fallbackID)
return
}
h.removeAuth(ctx, path)
}
func sameAuthFilePath(left, right string) bool {
left = cleanAuthFilePath(left)
right = cleanAuthFilePath(right)
if left == "" || right == "" {
return false
}
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func cleanAuthFilePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
if abs, errAbs := filepath.Abs(path); errAbs == nil && strings.TrimSpace(abs) != "" {
path = abs
}
return filepath.Clean(path)
}
func (h *Handler) deleteTokenRecord(ctx context.Context, path string) error {
if strings.TrimSpace(path) == "" {
return fmt.Errorf("auth path is empty")
}
store := h.tokenStoreWithBaseDir()
if store == nil {
return fmt.Errorf("token store unavailable")
}
return store.Delete(ctx, path)
}
func (h *Handler) tokenStoreWithBaseDir() coreauth.Store {
if h == nil {
return nil
}
store := h.tokenStore
if store == nil {
store = sdkAuth.GetTokenStore()
h.tokenStore = store
}
if h.cfg != nil {
if dirSetter, ok := store.(interface{ SetBaseDir(string) }); ok {
dirSetter.SetBaseDir(h.cfg.AuthDir)
}
}
return store
}
func (h *Handler) mergeExistingAuthFileMetadata(record *coreauth.Auth) {
if h == nil || record == nil {
return
}
var existingMap map[string]any
if h.cfg != nil && strings.TrimSpace(h.cfg.AuthDir) != "" {
targetFile := record.FileName
if targetFile == "" {
targetFile = record.ID
}
if targetFile != "" {
fullPath := filepath.Join(h.cfg.AuthDir, targetFile)
if raw, errRead := os.ReadFile(fullPath); errRead == nil && len(raw) > 0 {
_ = json.Unmarshal(raw, &existingMap)
}
}
}
if existingMap == nil && h.authManager != nil {
if existing, ok := h.authManager.GetByID(record.ID); ok && existing != nil && existing.Metadata != nil {
existingMap = existing.Metadata
} else {
for _, auth := range h.authManager.List() {
if auth != nil && auth.FileName == record.FileName && auth.Metadata != nil {
existingMap = auth.Metadata
break
}
}
}
}
if len(existingMap) > 0 {
coreauth.MergeExistingAuthMetadata(record, existingMap)
}
}
func (h *Handler) saveTokenRecord(ctx context.Context, record *coreauth.Auth) (string, error) {
if record == nil {
return "", fmt.Errorf("token record is nil")
}
h.mergeExistingAuthFileMetadata(record)
store := h.tokenStoreWithBaseDir()
if store == nil {
return "", fmt.Errorf("token store unavailable")
}
if h.postAuthHook != nil {
if err := h.postAuthHook(ctx, record); err != nil {
return "", fmt.Errorf("post-auth hook failed: %w", err)
}
}
savedPath, errSave := store.Save(ctx, record)
if errSave != nil {
return savedPath, errSave
}
if h.postAuthPersistHook != nil {
if errHook := h.postAuthPersistHook(ctx, record); errHook != nil {
return savedPath, fmt.Errorf("post-auth persist hook failed: %w", errHook)
}
}
return savedPath, nil
}