package handlers // v0.38.1: ConnectionResolver bridges the sandbox connections module // to the store + vault layers. Implements sandbox.ConnectionResolver. import ( "context" "encoding/json" "chat-switchboard/crypto" "chat-switchboard/models" "chat-switchboard/store" ) // ConnectionResolverAdapter implements sandbox.ConnectionResolver. type ConnectionResolverAdapter struct { stores store.Stores vault *crypto.KeyResolver } func NewConnectionResolverAdapter(s store.Stores, vault *crypto.KeyResolver) *ConnectionResolverAdapter { return &ConnectionResolverAdapter{stores: s, vault: vault} } // Resolve returns a single connection with secrets decrypted. func (r *ConnectionResolverAdapter) Resolve(ctx context.Context, userID, connType, name string) (*models.ExtConnection, error) { conn, err := r.stores.Connections.Resolve(ctx, userID, connType, name) if err != nil { return nil, err } r.decryptInPlace(ctx, conn) return conn, nil } // ResolveAll returns all accessible connections with secrets decrypted. func (r *ConnectionResolverAdapter) ResolveAll(ctx context.Context, userID, connType string) ([]models.ExtConnection, error) { conns, err := r.stores.Connections.ResolveAll(ctx, userID, connType) if err != nil { return nil, err } for i := range conns { r.decryptInPlace(ctx, &conns[i]) } return conns, nil } // decryptInPlace decrypts secret fields in the connection's config. func (r *ConnectionResolverAdapter) decryptInPlace(ctx context.Context, conn *models.ExtConnection) { if conn == nil || conn.Config == nil || r.vault == nil { return } secretFields := r.lookupSecretFields(ctx, conn.PackageID, conn.Type) for k, v := range conn.Config { if !secretFields[k] { continue } m, ok := v.(map[string]interface{}) if !ok { continue } enc := toBytes(m["enc"]) nonce := toBytes(m["nonce"]) if len(enc) == 0 { continue } plaintext, err := r.vault.Decrypt(enc, nonce, conn.Scope, conn.OwnerID) if err == nil { conn.Config[k] = plaintext } } } // lookupSecretFields reads the package manifest to identify secret fields. func (r *ConnectionResolverAdapter) lookupSecretFields(ctx context.Context, packageID, connType string) map[string]bool { result := map[string]bool{} pkg, err := r.stores.Packages.Get(ctx, packageID) if err != nil || pkg == nil { return result } connsRaw, ok := pkg.Manifest["connections"] if !ok { return result } connsJSON, _ := json.Marshal(connsRaw) var conns []struct { Type string `json:"type"` Fields map[string]map[string]string `json:"fields"` } json.Unmarshal(connsJSON, &conns) for _, cd := range conns { if cd.Type == connType { for fieldName, fieldDef := range cd.Fields { if fieldDef["type"] == "secret" { result[fieldName] = true } } break } } return result }